Coverage for pyTooling/Versioning/__init__.py: 89%
1392 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-03 23:02 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-03 23:02 +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 typing import Optional as Nullable, Union, Callable, Any, ClassVar, Generic, TypeVar, Iterable
51from typing import Iterator, Self
53from pyTooling.Decorators import export, readonly
54from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride
55from pyTooling.Exceptions import ToolingException
56from pyTooling.Common import getFullyQualifiedName
59@export
60class VersionValidatorError(ToolingException):
61 """
62 Raised when a parsed version is rejected by the validator it was parsed with.
64 The version string itself was well-formed - it parsed - so this is not a :exc:`ValueError` about the input, but
65 a statement that the resulting version is not acceptable to the caller. The version that failed is carried in
66 :attr:`Version`, so a caller can report what was wrong with it.
67 """
69 _version: Nullable[Version] #: The version rejected by a validator.
71 def __init__(self, message: str, /, *, version: Nullable[Version] = None) -> None:
72 """
73 Initializes the exception with the rejected version.
75 :param message: The exception's message.
76 :param version: Optional, the version the validator rejected.
77 """
78 super().__init__(message)
79 self._version = version
81 @readonly
82 def Version(self) -> Nullable[Version]:
83 """
84 Read-only property to access the version the validator rejected (:attr:`_version`).
86 :returns: The rejected version, or ``None`` if it wasn't recorded.
87 """
88 return self._version
91@export
92class Parts(Flag):
93 """Enumeration describing parts of a version number that can be present."""
94 Unknown = 0 #: Undocumented
95 Epoch = 1 #: Epoch is present. (e.g. E in ``E:1.2.3`` or ``vE!1.2.3``)
96 Major = 2 #: Major number is present. (e.g. X in ``vX.0.0``).
97 Year = 2 #: Year is present. (e.g. X in ``XXXX.10``).
98 Minor = 4 #: Minor number is present. (e.g. Y in ``v0.Y.0``).
99 Month = 4 #: Month is present. (e.g. X in ``2024.YY``).
100 Week = 4 #: Week is present. (e.g. X in ``2024.YY``).
101 Micro = 8 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
102 Patch = 8 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
103 Day = 8 #: Day is present. (e.g. X in ``2024.10.ZZ``).
104 Level = 16 #: Release level is present.
105 Dev = 32 #: Development part is present.
106 Build = 64 #: Build number is present. (e.g. bbbb in ``v0.0.0.bbbb``)
107 Post = 128 #: Post-release number is present.
108 Prefix = 256 #: Prefix is present.
109 Postfix = 512 #: Postfix is present.
110 Hash = 1024 #: Hash is present.
111# AHead = 256
114@export
115class ReleaseLevel(Enum):
116 """Enumeration describing the version's maturity level."""
117 Final = 0 #:
118 ReleaseCandidate = -10 #:
119 Development = -20 #:
120 Gamma = -30 #:
121 Beta = -40 #:
122 Alpha = -50 #:
124 def __eq__(self, other: Any) -> bool:
125 """
126 Compare two release levels if the level is equal to the second operand.
128 :param other: Operand to compare against.
129 :returns: ``True``, if release level is equal the second operand's release level.
130 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
131 """
132 if isinstance(other, str): 132 ↛ 133line 132 didn't jump to line 133 because the condition on line 132 was never true
133 other = ReleaseLevel(other)
135 if not isinstance(other, ReleaseLevel): 135 ↛ 136line 135 didn't jump to line 136 because the condition on line 135 was never true
136 ex = TypeError("Second operand is not supported by == operator.")
137 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
138 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
139 raise ex
141 return self is other
143 def __ne__(self, other: Any) -> bool:
144 """
145 Compare two release levels if the level is unequal to the second operand.
147 :param other: Operand to compare against.
148 :returns: ``True``, if release level is unequal the second operand's release level.
149 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
150 """
151 if isinstance(other, str):
152 other = ReleaseLevel(other)
154 if not isinstance(other, ReleaseLevel):
155 ex = TypeError("Second operand is not supported by != operator.")
156 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
157 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
158 raise ex
160 return self is not other
162 def __lt__(self, other: Any) -> bool:
163 """
164 Compare two release levels if the level is less than the second operand.
166 :param other: Operand to compare against.
167 :returns: ``True``, if release level is less than the second operand.
168 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
169 """
170 if isinstance(other, str): 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true
171 other = ReleaseLevel(other)
173 if not isinstance(other, ReleaseLevel): 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true
174 ex = TypeError("Second operand is not supported by < operator.")
175 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
176 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
177 raise ex
179 return self.value < other.value
181 def __le__(self, other: Any) -> bool:
182 """
183 Compare two release levels if the level is less than or equal the second operand.
185 :param other: Operand to compare against.
186 :returns: ``True``, if release level is less than or equal the second operand.
187 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
188 """
189 if isinstance(other, str):
190 other = ReleaseLevel(other)
192 if not isinstance(other, ReleaseLevel):
193 ex = TypeError("Second operand is not supported by <=>= operator.")
194 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
195 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
196 raise ex
198 return self.value <= other.value
200 def __gt__(self, other: Any) -> bool:
201 """
202 Compare two release levels if the level is greater than the second operand.
204 :param other: Operand to compare against.
205 :returns: ``True``, if release level is greater than the second operand.
206 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
207 """
208 if isinstance(other, str): 208 ↛ 209line 208 didn't jump to line 209 because the condition on line 208 was never true
209 other = ReleaseLevel(other)
211 if not isinstance(other, ReleaseLevel): 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true
212 ex = TypeError("Second operand is not supported by > operator.")
213 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
214 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
215 raise ex
217 return self.value > other.value
219 def __ge__(self, other: Any) -> bool:
220 """
221 Compare two release levels if the level is greater than or equal the second operand.
223 :param other: Operand to compare against.
224 :returns: ``True``, if release level is greater than or equal the second operand.
225 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
226 """
227 if isinstance(other, str):
228 other = ReleaseLevel(other)
230 if not isinstance(other, ReleaseLevel):
231 ex = TypeError("Second operand is not supported by >= operator.")
232 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
233 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
234 raise ex
236 return self.value >= other.value
238 def __hash__(self) -> int:
239 """
240 Compute a hash for this release level, so it can be used as a key in a dictionary or an element of a set.
242 The hash is derived from the release level's value, so two release levels compare and hash alike.
244 :returns: Hash of the release level's value.
245 """
246 return hash(self.value)
248 def __str__(self) -> str:
249 """
250 Returns the release level's string equivalent.
252 :returns: The string equivalent of the release level.
253 :raises ToolingException: If the release level is unknown, so it has no string equivalent.
254 """
255 if self is ReleaseLevel.Final:
256 return "final"
257 elif self is ReleaseLevel.ReleaseCandidate:
258 return "rc"
259 elif self is ReleaseLevel.Development: 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true
260 return "dev"
261 elif self is ReleaseLevel.Beta: 261 ↛ 262line 261 didn't jump to line 262 because the condition on line 261 was never true
262 return "beta"
263 elif self is ReleaseLevel.Alpha: 263 ↛ 266line 263 didn't jump to line 266 because the condition on line 263 was always true
264 return "alpha"
266 raise ToolingException(f"Unknown ReleaseLevel '{self.name}'.")
269@export
270class Flags(Flag):
271 """State enumeration, if a (tagged) version is build from a clean or dirty working directory."""
272 NoVCS = 0 #: No Version Control System VCS
273 Clean = 1 #: A versioned build was created from a *clean* working directory.
274 Dirty = 2 #: A versioned build was created from a *dirty* working directory.
276 CVS = 16 #: Concurrent Versions System (CVS)
277 SVN = 32 #: Subversion (SVN)
278 Git = 64 #: Git
279 Hg = 128 #: Mercurial (Hg)
282@export
283def WordSizeValidator(
284 bits: Nullable[int] = None,
285 majorBits: Nullable[int] = None,
286 minorBits: Nullable[int] = None,
287 microBits: Nullable[int] = None,
288 buildBits: Nullable[int] = None
289):
290 """
291 A factory function to return a validator for Version instances for a positive integer range based on word-sizes in bits.
293 :param bits: Optional, number of bits to encode any positive version number part.
294 :param majorBits: Optional, number of bits to encode a positive major number in a version.
295 :param minorBits: Optional, number of bits to encode a positive minor number in a version.
296 :param microBits: Optional, number of bits to encode a positive micro number in a version.
297 :param buildBits: Optional, number of bits to encode a positive build number in a version.
298 :returns: A validation function for Version instances.
299 """
300 majorMax = minorMax = microMax = buildMax = -1
301 if bits is not None:
302 majorMax = minorMax = microMax = buildMax = 2**bits - 1
304 if majorBits is not None:
305 majorMax = 2**majorBits - 1
307 if minorBits is not None:
308 minorMax = 2**minorBits - 1
310 if microBits is not None:
311 microMax = 2 ** microBits - 1
313 if buildBits is not None: 313 ↛ 314line 313 didn't jump to line 314 because the condition on line 313 was never true
314 buildMax = 2**buildBits - 1
316 def validator(version: SemanticVersion) -> bool:
317 """
318 Validator function, which checks each version part against the maximum its word size allows.
320 :param version: Optional, the version to validate.
321 :returns: ``True``, if every part fits into its word size.
322 :raises ValueError: If a part exceeds the maximum value of its word size.
323 """
324 if Parts.Major in version._parts and version._major > majorMax:
325 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
327 if Parts.Minor in version._parts and version._minor > minorMax:
328 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
330 if Parts.Micro in version._parts and version._micro > microMax:
331 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
333 if Parts.Build in version._parts and version._build > buildMax: 333 ↛ 334line 333 didn't jump to line 334 because the condition on line 333 was never true
334 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
336 return True
338 return validator
341@export
342def MaxValueValidator(
343 max: Nullable[int] = None,
344 majorMax: Nullable[int] = None,
345 minorMax: Nullable[int] = None,
346 microMax: Nullable[int] = None,
347 buildMax: Nullable[int] = None
348):
349 """
350 A factory function to return a validator for Version instances checking for a positive integer range [0..max].
352 :param max: Optional, the upper bound for any positive version number part.
353 :param majorMax: Optional, the upper bound for the positive major number.
354 :param minorMax: Optional, the upper bound for the positive minor number.
355 :param microMax: Optional, the upper bound for the positive micro number.
356 :param buildMax: Optional, the upper bound for the positive build number.
357 :returns: A validation function for Version instances.
358 """
359 if max is not None: 359 ↛ 362line 359 didn't jump to line 362 because the condition on line 359 was always true
360 majorMax = minorMax = microMax = buildMax = max
362 def validator(version: SemanticVersion) -> bool:
363 """
364 Validator function, which checks each version part against its maximum value.
366 :param version: Optional, the version to validate.
367 :returns: ``True``, if every part is within its maximum.
368 :raises ValueError: If a part exceeds its maximum value.
369 """
370 if Parts.Major in version._parts and version._major > majorMax:
371 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
373 if Parts.Minor in version._parts and version._minor > minorMax:
374 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
376 if Parts.Micro in version._parts and version._micro > microMax:
377 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
379 if Parts.Build in version._parts and version._build > buildMax: 379 ↛ 380line 379 didn't jump to line 380 because the condition on line 379 was never true
380 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
382 return True
384 return validator
387@export
388class Version(metaclass=ExtendedType, slots=True):
389 """Base-class for a version representation."""
391 __hash: Nullable[int] #: once computed hash of the object
393 #: Separator between an epoch and the rest of the version number. Debian writes ``2:1.2.3``; PEP 440 writes
394 #: ``2!1.2.3``, so :class:`PythonVersion` overrides this.
395 _EPOCH_SEPARATOR: ClassVar[str] = ":"
397 _parts: Parts #: Integer flag enumeration of present parts in a version number.
398 _prefix: str #: Prefix string
399 _epoch: int #: Epoch, which outranks every other part of the version number.
400 _major: int #: Major number part of the version number.
401 _minor: int #: Minor number part of the version number.
402 _micro: int #: Micro number part of the version number.
403 _releaseLevel: ReleaseLevel #: Release level (alpha, beta, rc, final, ...).
404 _releaseNumber: int #: Release number (Python calls this a serial).
405 _post: int #: Post-release version number part.
406 _dev: int #: Development number
407 _build: int #: Build number part of the version number.
408 _postfix: str #: Postfix string
409 _hash: str #: Hash from version control system.
410 _flags: Flags #: State if the version in a working directory is clean or dirty compared to a tagged version.
412 def __init__(
413 self,
414 major: int,
415 minor: Nullable[int] = None,
416 micro: Nullable[int] = None,
417 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
418 number: Nullable[int] = None,
419 post: Nullable[int] = None,
420 dev: Nullable[int] = None,
421 *,
422 epoch: Nullable[int] = None,
423 build: Nullable[int] = None,
424 postfix: Nullable[str] = None,
425 prefix: Nullable[str] = None,
426 hash: Nullable[str] = None,
427 flags: Flags = Flags.NoVCS
428 ) -> None:
429 """
430 Initializes a version number representation.
432 :param major: Major number part of the version number.
433 :param minor: Optional, minor number part of the version number.
434 :param micro: Optional, micro (patch) number part of the version number.
435 :param level: Optional, release level (alpha, beta, release candidate, final, ...) of the version number.
436 :param number: Optional, release number part (in combination with release level) of the version number.
437 :param post: Optional, post number part of the version number.
438 :param dev: Optional, development number part of the version number.
439 :param epoch: Optional, the version number's epoch, which outranks every other part.
440 :param build: Optional, build number part of the version number.
441 :param postfix: Optional, the version number's postfix.
442 :param prefix: Optional, the version number's prefix.
443 :param hash: Optional, postfix string.
444 :param flags: Optional, the version number's flags.
445 :raises TypeError: If parameter 'major' is not of type integer.
446 :raises ValueError: If parameter 'major' is a negative number.
447 :raises TypeError: If parameter 'minor' is not of type integer.
448 :raises ValueError: If parameter 'minor' is a negative number.
449 :raises TypeError: If parameter 'micro' is not of type integer.
450 :raises ValueError: If parameter 'micro' is a negative number.
451 :raises TypeError: If parameter 'epoch' is not of type integer.
452 :raises ValueError: If parameter 'epoch' is a negative number.
453 :raises TypeError: If parameter 'build' is not of type integer.
454 :raises ValueError: If parameter 'build' is a negative number.
455 :raises TypeError: If parameter 'prefix' is not of type string.
456 :raises TypeError: If parameter 'postfix' is not of type string.
457 """
458 self.__hash = None
460 if not isinstance(major, int):
461 ex = TypeError("Parameter 'major' is not of type 'int'.")
462 ex.add_note(f"Got type '{getFullyQualifiedName(major)}'.")
463 raise ex
464 elif major < 0:
465 raise ValueError("Parameter 'major' is negative.")
467 self._parts = Parts.Major
468 self._major = major
470 if epoch is not None:
471 if not isinstance(epoch, int):
472 ex = TypeError("Parameter 'epoch' is not of type 'int'.")
473 ex.add_note(f"Got type '{getFullyQualifiedName(epoch)}'.")
474 raise ex
475 elif epoch < 0:
476 raise ValueError("Parameter 'epoch' is negative.")
478 self._parts |= Parts.Epoch
479 self._epoch = epoch
480 else:
481 self._epoch = 0
483 if minor is not None:
484 if not isinstance(minor, int):
485 ex = TypeError("Parameter 'minor' is not of type 'int'.")
486 ex.add_note(f"Got type '{getFullyQualifiedName(minor)}'.")
487 raise ex
488 elif minor < 0:
489 raise ValueError("Parameter 'minor' is negative.")
491 self._parts |= Parts.Minor
492 self._minor = minor
493 else:
494 self._minor = 0
496 if micro is not None:
497 if not isinstance(micro, int):
498 ex = TypeError("Parameter 'micro' is not of type 'int'.")
499 ex.add_note(f"Got type '{getFullyQualifiedName(micro)}'.")
500 raise ex
501 elif micro < 0:
502 raise ValueError("Parameter 'micro' is negative.")
504 self._parts |= Parts.Micro
505 self._micro = micro
506 else:
507 self._micro = 0
509 if level is None:
510 raise ValueError("Parameter 'level' is None.")
511 elif not isinstance(level, ReleaseLevel):
512 ex = TypeError("Parameter 'level' is not of type 'ReleaseLevel'.")
513 ex.add_note(f"Got type '{getFullyQualifiedName(level)}'.")
514 raise ex
515 elif level is ReleaseLevel.Final:
516 if number is not None:
517 raise ValueError("Parameter 'number' must be None, if parameter 'level' is 'Final'.")
519 self._parts |= Parts.Level
520 self._releaseLevel = level
521 self._releaseNumber = 0
522 else:
523 self._parts |= Parts.Level
524 self._releaseLevel = level
526 if number is not None:
527 if not isinstance(number, int):
528 ex = TypeError("Parameter 'number' is not of type 'int'.")
529 ex.add_note(f"Got type '{getFullyQualifiedName(number)}'.")
530 raise ex
531 elif number < 0:
532 raise ValueError("Parameter 'number' is negative.")
534 self._releaseNumber = number
535 else:
536 self._releaseNumber = 0
538 if dev is not None:
539 if not isinstance(dev, int):
540 ex = TypeError("Parameter 'dev' is not of type 'int'.")
541 ex.add_note(f"Got type '{getFullyQualifiedName(dev)}'.")
542 raise ex
543 elif dev < 0:
544 raise ValueError("Parameter 'dev' is negative.")
546 self._parts |= Parts.Dev
547 self._dev = dev
548 else:
549 self._dev = 0
551 if post is not None:
552 if not isinstance(post, int):
553 ex = TypeError("Parameter 'post' is not of type 'int'.")
554 ex.add_note(f"Got type '{getFullyQualifiedName(post)}'.")
555 raise ex
556 elif post < 0:
557 raise ValueError("Parameter 'post' is negative.")
559 self._parts |= Parts.Post
560 self._post = post
561 else:
562 self._post = 0
564 if build is not None:
565 if not isinstance(build, int):
566 ex = TypeError("Parameter 'build' is not of type 'int'.")
567 ex.add_note(f"Got type '{getFullyQualifiedName(build)}'.")
568 raise ex
569 elif build < 0:
570 raise ValueError("Parameter 'build' is negative.")
572 self._build = build
573 self._parts |= Parts.Build
574 else:
575 self._build = 0
577 if postfix is not None:
578 if not isinstance(postfix, str):
579 ex = TypeError("Parameter 'postfix' is not of type 'str'.")
580 ex.add_note(f"Got type '{getFullyQualifiedName(postfix)}'.")
581 raise ex
583 self._parts |= Parts.Postfix
584 self._postfix = postfix
585 else:
586 self._postfix = ""
588 if prefix is not None:
589 if not isinstance(prefix, str):
590 ex = TypeError("Parameter 'prefix' is not of type 'str'.")
591 ex.add_note(f"Got type '{getFullyQualifiedName(prefix)}'.")
592 raise ex
594 self._parts |= Parts.Prefix
595 self._prefix = prefix
596 else:
597 self._prefix = ""
599 if hash is not None:
600 if not isinstance(hash, str):
601 ex = TypeError("Parameter 'hash' is not of type 'str'.")
602 ex.add_note(f"Got type '{getFullyQualifiedName(hash)}'.")
603 raise ex
605 self._parts |= Parts.Hash
606 self._hash = hash
607 else:
608 self._hash = ""
610 if flags is None:
611 raise ValueError("Parameter 'flags' is None.")
612 elif not isinstance(flags, Flags):
613 ex = TypeError("Parameter 'flags' is not of type 'Flags'.")
614 ex.add_note(f"Got type '{getFullyQualifiedName(flags)}'.")
615 raise ex
617 self._flags = flags
619 @classmethod
620 @abstractmethod
621 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[[SemanticVersion], bool]] = None) -> Version:
622 """
623 Parse a version string and return a Version instance.
625 :param versionString: The version string to parse.
626 :param validator: Optional, validator rejecting a parsed version, e.g. by word size or maximum value.
627 :returns: The parsed version number.
628 """
630 @readonly
631 def Parts(self) -> Parts:
632 """
633 Read-only property to access the used parts of this version number.
635 :returns: A flag enumeration of used version number parts.
636 """
637 return self._parts
639 @readonly
640 def Prefix(self) -> str:
641 """
642 Read-only property to access the version number's prefix.
644 :returns: The prefix of the version number.
645 """
646 return self._prefix
648 @readonly
649 def Epoch(self) -> int:
650 """
651 Read-only property to access the epoch (:attr:`_epoch`).
653 An epoch outranks every other part, so a version carrying one is newer than any version with a lower epoch
654 however high the rest of its numbers are. It exists so a project can *lower* its version number - change
655 scheme, or recover from a bad release - without every later version comparing as older. A version without one
656 has epoch ``0``, which is what keeps it comparable with a version that has one.
658 :returns: The epoch, or ``0`` if the version carries none.
659 """
660 return self._epoch
662 @readonly
663 def Major(self) -> int:
664 """
665 Read-only property to access the major number.
667 :returns: The major number.
668 """
669 return self._major
671 @readonly
672 def Minor(self) -> int:
673 """
674 Read-only property to access the minor number.
676 :returns: The minor number.
677 """
678 return self._minor
680 @readonly
681 def Micro(self) -> int:
682 """
683 Read-only property to access the micro number.
685 :returns: The micro number.
686 """
687 return self._micro
689 @readonly
690 def ReleaseLevel(self) -> ReleaseLevel:
691 """
692 Read-only property to access the release level.
694 :returns: The release level.
695 """
696 return self._releaseLevel
698 @readonly
699 def ReleaseNumber(self) -> int:
700 """
701 Read-only property to access the release number.
703 :returns: The release number.
704 """
705 return self._releaseNumber
707 @readonly
708 def Post(self) -> int:
709 """
710 Read-only property to access the post number.
712 :returns: The post number.
713 """
714 return self._post
716 @readonly
717 def Dev(self) -> int:
718 """
719 Read-only property to access the development number.
721 :returns: The development number.
722 """
723 return self._dev
725 @readonly
726 def Build(self) -> int:
727 """
728 Read-only property to access the build number.
730 :returns: The build number.
731 """
732 return self._build
734 @readonly
735 def Postfix(self) -> str:
736 """
737 Read-only property to access the version number's postfix.
739 :returns: The postfix of the version number.
740 """
741 return self._postfix
743 @readonly
744 def Hash(self) -> str:
745 """
746 Read-only property to access the version number's hash.
748 :returns: The hash.
749 """
750 return self._hash
752 @readonly
753 def Flags(self) -> Flags:
754 """
755 Read-only property to access the version number's flags.
757 :returns: The flags of the version number.
758 """
759 return self._flags
761 def _equal(self, left: Version, right: Version) -> Nullable[bool]:
762 """
763 Private helper method to compute the equality of two :class:`Version` instances.
765 :param left: Left operand.
766 :param right: Right operand.
767 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
768 """
769 return (
770 (left._epoch == right._epoch) and
771 (left._major == right._major) and
772 (left._minor == right._minor) and
773 (left._micro == right._micro) and
774 (left._releaseLevel == right._releaseLevel) and
775 (left._releaseNumber == right._releaseNumber) and
776 (left._post == right._post) and
777 (left._dev == right._dev) and
778 (left._build == right._build) and
779 (left._postfix == right._postfix)
780 )
782 def _compare(self, left: Version, right: Version) -> Nullable[bool]:
783 """
784 Private helper method to compute the comparison of two :class:`Version` instances.
786 :param left: Left operand.
787 :param right: Right operand.
788 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
789 False if ``left`` is greater than ``right``. |br|
790 Otherwise it's None (both operands are equal).
791 """
792 if left._epoch < right._epoch:
793 return True
794 elif left._epoch > right._epoch:
795 return False
797 if left._major < right._major:
798 return True
799 elif left._major > right._major:
800 return False
802 if left._minor < right._minor:
803 return True
804 elif left._minor > right._minor:
805 return False
807 if left._micro < right._micro:
808 return True
809 elif left._micro > right._micro:
810 return False
812 if left._releaseLevel < right._releaseLevel: 812 ↛ 813line 812 didn't jump to line 813 because the condition on line 812 was never true
813 return True
814 elif left._releaseLevel > right._releaseLevel: 814 ↛ 815line 814 didn't jump to line 815 because the condition on line 814 was never true
815 return False
817 if left._releaseNumber < right._releaseNumber: 817 ↛ 818line 817 didn't jump to line 818 because the condition on line 817 was never true
818 return True
819 elif left._releaseNumber > right._releaseNumber: 819 ↛ 820line 819 didn't jump to line 820 because the condition on line 819 was never true
820 return False
822 if left._post < right._post: 822 ↛ 823line 822 didn't jump to line 823 because the condition on line 822 was never true
823 return True
824 elif left._post > right._post: 824 ↛ 825line 824 didn't jump to line 825 because the condition on line 824 was never true
825 return False
827 if left._dev < right._dev: 827 ↛ 828line 827 didn't jump to line 828 because the condition on line 827 was never true
828 return True
829 elif left._dev > right._dev: 829 ↛ 830line 829 didn't jump to line 830 because the condition on line 829 was never true
830 return False
832 if left._build < right._build:
833 return True
834 elif left._build > right._build:
835 return False
837 return None
839 def _minimum(self, actual: Version, expected: Version) -> Nullable[bool]:
840 """
841 Check if a version fulfills a minimum requirement.
843 How exact the comparison is depends on how detailed the expected version is: a minor number in the expectation
844 requires an exact major number, and a micro number requires an exact minor number.
846 :param actual: The version to check.
847 :param expected: The minimum version, whose parts decide how exact the comparison is.
848 :returns: ``True``, if the actual version fulfills the expectation.
849 """
850 exactMajor = Parts.Minor in expected._parts
851 exactMinor = Parts.Micro in expected._parts
853 if exactMajor and actual._major != expected._major: 853 ↛ 854line 853 didn't jump to line 854 because the condition on line 853 was never true
854 return False
855 elif not exactMajor and actual._major < expected._major:
856 return False
858 if exactMinor and actual._minor != expected._minor: 858 ↛ 859line 858 didn't jump to line 859 because the condition on line 858 was never true
859 return False
860 elif not exactMinor and actual._minor < expected._minor:
861 return False
863 if Parts.Micro in expected._parts:
864 return actual._micro >= expected._micro
866 return True
868 def _format(self, formatSpec: str) -> str:
869 """
870 Return a string representation of this version number according to the format specification.
872 .. topic:: Format Specifiers
874 * ``%p`` - prefix
875 * ``%M`` - major number
876 * ``%m`` - minor number
877 * ``%u`` - micro number
878 * ``%b`` - build number
880 :param formatSpec: The format specification.
881 :returns: Formatted version number.
882 """
883 if formatSpec == "":
884 return self.__str__()
886 result = formatSpec
887 result = result.replace("%p", str(self._prefix))
888 result = result.replace("%M", str(self._major))
889 result = result.replace("%m", str(self._minor))
890 result = result.replace("%u", str(self._micro))
891 result = result.replace("%b", str(self._build))
892 result = result.replace("%r", str(self._releaseLevel)[0])
893 result = result.replace("%R", str(self._releaseLevel))
894 result = result.replace("%n", str(self._releaseNumber))
895 result = result.replace("%d", str(self._dev))
896 result = result.replace("%P", str(self._postfix))
898 return result
900 @mustoverride
901 def __eq__(self, other: Any) -> bool:
902 """
903 Compare two version numbers for equality.
905 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
906 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
907 number is assumed (all other parts are zero).
909 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
910 number.
912 :param other: Operand to compare against.
913 :returns: ``True``, if both version numbers are equal.
914 :raises ValueError: If parameter ``other`` is None.
915 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, string or integer.
916 """
917 if other is None:
918 raise ValueError("Second operand is None.")
919 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
920 pass
921 elif isinstance(other, str):
922 other = self.__class__.Parse(other)
923 elif isinstance(other, int):
924 other = self.__class__(major=other)
925 else:
926 ex = TypeError("Second operand is not supported by == operator.")
927 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
928 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
929 raise ex
931 return self._equal(self, other)
933 @mustoverride
934 def __ne__(self, other: Any) -> bool:
935 """
936 Compare two version numbers for inequality.
938 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
939 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
940 number is assumed (all other parts are zero).
942 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
943 number.
945 :param other: Operand to compare against.
946 :returns: ``True``, if both version numbers are not equal.
947 :raises ValueError: If parameter ``other`` is None.
948 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, string or integer.
949 """
950 if other is None:
951 raise ValueError("Second operand is None.")
952 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
953 pass
954 elif isinstance(other, str):
955 other = self.__class__.Parse(other)
956 elif isinstance(other, int):
957 other = self.__class__(major=other)
958 else:
959 ex = TypeError("Second operand is not supported by == operator.")
960 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
961 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
962 raise ex
964 return not self._equal(self, other)
966 @mustoverride
967 def __lt__(self, other: Any) -> bool:
968 """
969 Compare two version numbers if the version is less than the second operand.
971 The second operand should be an instance of :class:`Version`, but :class:`VersionRange`, :class:`VersionSet`,
972 ``str`` and ``int`` are accepted, too. |br|
973 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
974 number is assumed (all other parts are zero).
976 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
977 number.
979 :param other: Operand to compare against.
980 :returns: ``True``, if version is less than the second operand.
981 :raises ValueError: If parameter ``other`` is None.
982 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`,
983 :class:`VersionSet`, string or integer.
984 """
985 if other is None:
986 raise ValueError("Second operand is None.")
987 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
988 pass
989 elif isinstance(other, VersionRange):
990 other = other._lowerBound
991 elif isinstance(other, VersionSet):
992 other = other._items[0]
993 elif isinstance(other, str):
994 other = self.__class__.Parse(other)
995 elif isinstance(other, int):
996 other = self.__class__(major=other)
997 else:
998 ex = TypeError("Second operand is not supported by < operator.")
999 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1000 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
1001 raise ex
1003 return self._compare(self, other) is True
1005 @mustoverride
1006 def __le__(self, other: Any) -> bool:
1007 """
1008 Compare two version numbers if the version is less than or equal the second operand.
1010 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
1011 ``str`` and ``int`` are accepted, too. |br|
1012 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
1013 number is assumed (all other parts are zero).
1015 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1016 number.
1018 :param other: Operand to compare against.
1019 :returns: ``True``, if version is less than or equal the second operand.
1020 :raises ValueError: If parameter ``other`` is None.
1021 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`,
1022 :class:`VersionSet`, string or integer.
1023 """
1024 equalValue = True
1025 if other is None:
1026 raise ValueError("Second operand is None.")
1027 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
1028 pass
1029 elif isinstance(other, VersionRange):
1030 equalValue = RangeBoundHandling.LowerBoundExclusive not in other._boundHandling
1031 other = other._lowerBound
1032 elif isinstance(other, VersionSet):
1033 other = other._items[0]
1034 elif isinstance(other, str):
1035 other = self.__class__.Parse(other)
1036 elif isinstance(other, int):
1037 other = self.__class__(major=other)
1038 else:
1039 ex = TypeError("Second operand is not supported by <= operator.")
1040 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1041 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
1042 raise ex
1044 result = self._compare(self, other)
1045 return result if result is not None else equalValue
1047 @mustoverride
1048 def __gt__(self, other: Any) -> bool:
1049 """
1050 Compare two version numbers if the version is greater than the second operand.
1052 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
1053 ``str`` and ``int`` are accepted, too. |br|
1054 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
1055 number is assumed (all other parts are zero).
1057 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1058 number.
1060 :param other: Operand to compare against.
1061 :returns: ``True``, if version is greater than the second operand.
1062 :raises ValueError: If parameter ``other`` is None.
1063 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`,
1064 :class:`VersionSet`, string or integer.
1065 """
1066 if other is None:
1067 raise ValueError("Second operand is None.")
1068 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
1069 pass
1070 elif isinstance(other, VersionRange):
1071 other = other._upperBound
1072 elif isinstance(other, VersionSet):
1073 other = other._items[-1]
1074 elif isinstance(other, str):
1075 other = self.__class__.Parse(other)
1076 elif isinstance(other, int):
1077 other = self.__class__(major=other)
1078 else:
1079 ex = TypeError("Second operand is not supported by > operator.")
1080 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1081 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
1082 raise ex
1084 return self._compare(self, other) is False
1086 @mustoverride
1087 def __ge__(self, other: Any) -> bool:
1088 """
1089 Compare two version numbers if the version is greater than or equal the second operand.
1091 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
1092 ``str`` and ``int`` are accepted, too. |br|
1093 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
1094 number is assumed (all other parts are zero).
1096 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1097 number.
1099 :param other: Operand to compare against.
1100 :returns: ``True``, if version is greater than or equal the second operand.
1101 :raises ValueError: If parameter ``other`` is None.
1102 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`,
1103 :class:`VersionSet`, string or integer.
1104 """
1105 equalValue = True
1106 if other is None:
1107 raise ValueError("Second operand is None.")
1108 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
1109 pass
1110 elif isinstance(other, VersionRange):
1111 equalValue = RangeBoundHandling.UpperBoundExclusive not in other._boundHandling
1112 other = other._upperBound
1113 elif isinstance(other, VersionSet):
1114 other = other._items[-1]
1115 elif isinstance(other, str):
1116 other = self.__class__.Parse(other)
1117 elif isinstance(other, int):
1118 other = self.__class__(major=other)
1119 else:
1120 ex = TypeError("Second operand is not supported by >= operator.")
1121 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1122 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
1123 raise ex
1125 result = self._compare(self, other)
1126 return not result if result is not None else equalValue
1128 def __rshift__(self, other: Union[Version, str, int, None]) -> bool:
1129 """
1130 Return the minimum of this version and a second operand.
1132 :param other: Second operand, a version, a version string or a major version number.
1133 :returns: ``True``, if this version is the minimum of both operands.
1134 :raises ValueError: If the second operand is ``None``.
1135 :raises TypeError: If the second operand is not a version, a string or an integer.
1136 """
1137 if other is None:
1138 raise ValueError("Second operand is None.")
1139 elif isinstance(other, self.__class__):
1140 pass
1141 elif isinstance(other, str):
1142 other = self.__class__.Parse(other)
1143 elif isinstance(other, int):
1144 other = self.__class__(major=other)
1145 else:
1146 ex = TypeError("Second operand is not supported by >> operator.")
1147 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1148 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
1149 raise ex
1151 return self._minimum(self, other)
1153 def __hash__(self) -> int:
1154 """
1155 Compute a hash for this version number and cache it.
1157 All parts of the version are part of the hash, so two versions differing in a postfix or a build number don't
1158 collide.
1160 :returns: Hash of this version number.
1161 """
1162 if self.__hash is None:
1163 self.__hash = hash((
1164 self._prefix,
1165 self._epoch,
1166 self._major,
1167 self._minor,
1168 self._micro,
1169 self._releaseLevel,
1170 self._releaseNumber,
1171 self._post,
1172 self._dev,
1173 self._build,
1174 self._postfix,
1175 self._hash,
1176 self._flags
1177 ))
1178 return self.__hash
1181@export
1182class SemanticVersion(Version):
1183 """Representation of a semantic version number like ``3.7.12``."""
1185 _PATTERN: ClassVar[Pattern] = re_compile(
1186 r"^"
1187 r"(?P<prefix>rev|REV|[vViIrR])?"
1188 r"(?:(?P<epoch>\d+):)?"
1189 r"(?P<major>\d+)"
1190 r"(?:\.(?P<minor>\d+))?"
1191 r"(?:\.(?P<micro>\d+))?"
1192 r"(?:"
1193 r"(?:\.(?P<build>\d+))"
1194 r"|"
1195 r"(?:[-](?P<release>dev|final))"
1196 r"|"
1197 r"(?:(?P<delim1>[\.\-]?)(?P<level>alpha|beta|gamma|a|b|c|rc|pl)(?P<number>\d+))"
1198 r")?"
1199 r"(?:(?P<delim2>[\.\-]post)(?P<post>\d+))?"
1200 r"(?:(?P<delim3>[\.\-]dev)(?P<dev>\d+))?"
1201 r"(?:(?P<delim4>[\.\-\+])(?P<postfix>\w+))?"
1202 r"$"
1203 ) #: Regular expression to parse a semantic version from a string.
1204# QUESTION: was this how many commits a version is ahead of the last tagged version?
1205# ahead: int = 0
1207 def __init_subclass__(cls, **kwargs: Any) -> None:
1208 """
1209 Rebuild the pattern when a derived class spells the epoch separator differently.
1211 A compiled pattern still carries the expression it was built from, so the base class' one is taken apart and
1212 put back together with this class' separator. Only the epoch's separator is substituted, and only when the
1213 class states no pattern of its own - a class replacing the whole expression means it.
1215 :param kwargs: Keyword arguments passed on to the base implementation.
1216 """
1217 super().__init_subclass__(**kwargs)
1219 if "_PATTERN" in cls.__dict__ or cls._EPOCH_SEPARATOR == SemanticVersion._EPOCH_SEPARATOR:
1220 return
1222 cls._PATTERN = re_compile(SemanticVersion._PATTERN.pattern.replace(
1223 f"(?P<epoch>\\d+){re_escape(SemanticVersion._EPOCH_SEPARATOR)}",
1224 f"(?P<epoch>\\d+){re_escape(cls._EPOCH_SEPARATOR)}"
1225 ))
1227 def __init__(
1228 self,
1229 major: int,
1230 minor: Nullable[int] = None,
1231 micro: Nullable[int] = None,
1232 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
1233 number: Nullable[int] = None,
1234 post: Nullable[int] = None,
1235 dev: Nullable[int] = None,
1236 *,
1237 epoch: Nullable[int] = None,
1238 build: Nullable[int] = None,
1239 postfix: Nullable[str] = None,
1240 prefix: Nullable[str] = None,
1241 hash: Nullable[str] = None,
1242 flags: Flags = Flags.NoVCS
1243 ) -> None:
1244 """
1245 Initializes a semantic version number representation.
1247 :param major: Major number part of the version number.
1248 :param minor: Optional, minor number part of the version number.
1249 :param micro: Optional, micro (patch) number part of the version number.
1250 :param level: Optional, release level of the version number (alpha, beta, release candidate, final, ...).
1251 :param number: Optional, number within the release level, e.g. ``2`` in ``rc2``.
1252 :param post: Optional, post number part of the version number.
1253 :param dev: Optional, development number part of the version number.
1254 :param epoch: Optional, the version number's epoch, which outranks every other part.
1255 :param build: Optional, build number part of the version number.
1256 :param postfix: Optional, the version number's postfix.
1257 :param prefix: Optional, the version number's prefix.
1258 :param hash: Optional, hash of the version control system's commit this version was built from.
1259 :param flags: Optional, the version number's flags.
1260 :raises TypeError: If parameter 'major' is not of type integer.
1261 :raises ValueError: If parameter 'major' is a negative number.
1262 :raises TypeError: If parameter 'minor' is not of type integer.
1263 :raises ValueError: If parameter 'minor' is a negative number.
1264 :raises TypeError: If parameter 'micro' is not of type integer.
1265 :raises ValueError: If parameter 'micro' is a negative number.
1266 :raises TypeError: If parameter 'post' is not of type integer.
1267 :raises ValueError: If parameter 'post' is a negative number.
1268 :raises TypeError: If parameter 'dev' is not of type integer.
1269 :raises ValueError: If parameter 'dev' is a negative number.
1270 :raises TypeError: If parameter 'epoch' is not of type integer.
1271 :raises ValueError: If parameter 'epoch' is a negative number.
1272 :raises TypeError: If parameter 'build' is not of type integer.
1273 :raises ValueError: If parameter 'build' is a negative number.
1274 :raises TypeError: If parameter 'prefix' is not of type string.
1275 :raises TypeError: If parameter 'postfix' is not of type string.
1276 """
1277 super().__init__(major, minor, micro, level, number, post, dev, epoch=epoch, build=build, postfix=postfix,
1278 prefix=prefix, hash=hash, flags=flags)
1280 @classmethod
1281 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[[SemanticVersion], bool]] = None) -> SemanticVersion:
1282 """
1283 Parse a version string and return a :class:`SemanticVersion` instance.
1285 Allowed prefix characters:
1287 * ``v|V`` - version, public version, public release
1288 * ``i|I`` - internal version, internal release
1289 * ``r|R`` - release, revision
1290 * ``rev|REV`` - revision
1292 :param versionString: The version string to parse.
1293 :param validator: Optional, a validation function.
1294 :returns: An object representing a semantic version.
1295 :raises TypeError: When parameter ``versionString`` is not a string.
1296 :raises ValueError: When parameter ``versionString`` is None or empty.
1297 :raises ValueError: When parameter ``versionString`` isn't a semantic version number. |br|
1298 It may carry one of the prefixes ``v``, ``i``, ``r`` or ``rev``, e.g. ``v1.2.3``.
1299 :raises ValueError: When the epoch is malformed. |br|
1300 An epoch is a number followed by the separator and comes after any prefix,
1301 e.g. ``2:1.2.3`` or ``v2!1.2.3``.
1302 :raises VersionValidatorError: When the parsed version is rejected by ``validator``.
1303 """
1304 if versionString is None:
1305 raise ValueError("Parameter 'versionString' is None.")
1306 elif not isinstance(versionString, str):
1307 ex = TypeError("Parameter 'versionString' is not of type 'str'.")
1308 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1309 raise ex
1310 elif (versionString := versionString.strip()) == "":
1311 raise ValueError("Parameter 'versionString' is empty.")
1313 if (match := cls._PATTERN.match(versionString)) is None:
1314 ex = ValueError(f"Syntax error in parameter 'versionString': '{versionString}'")
1315 ex.add_note("It may carry one of the prefixes 'v', 'i', 'r' or 'rev', e.g. 'v1.2.3'.")
1316 raise ex
1318 def toInt(value: Nullable[str]) -> Nullable[int]:
1319 """
1320 Nested function converting an optional part of a version string to an integer.
1322 :param value: The matched part, or ``None`` if the pattern didn't match it.
1323 :returns: The part as an integer, or ``None`` if it wasn't present.
1324 :raises ValueError: If the part isn't a number.
1325 """
1326 if value is None or value == "":
1327 return None
1329 try:
1330 return int(value)
1331 except ValueError as ex: # pragma: no cover
1332 raise ValueError(f"Invalid part '{value}' in version number '{versionString}'.") from ex
1334 prefix = match["prefix"]
1336 release = match["release"]
1337 if release is not None:
1338 if release == "dev": 1338 ↛ 1340line 1338 didn't jump to line 1340 because the condition on line 1338 was always true
1339 releaseLevel = ReleaseLevel.Development
1340 elif release == "final":
1341 releaseLevel = ReleaseLevel.Final
1342 else: # pragma: no cover
1343 raise ValueError(f"Unknown release level '{release}' in version number '{versionString}'.")
1344 else:
1345 level = match["level"]
1346 if level is not None:
1347 level = level.lower()
1348 if level == "a" or level == "alpha":
1349 releaseLevel = ReleaseLevel.Alpha
1350 elif level == "b" or level == "beta":
1351 releaseLevel = ReleaseLevel.Beta
1352 elif level == "c" or level == "gamma":
1353 releaseLevel = ReleaseLevel.Gamma
1354 elif level == "rc":
1355 releaseLevel = ReleaseLevel.ReleaseCandidate
1356 else: # pragma: no cover
1357 raise ValueError(f"Unknown release level '{level}' in version number '{versionString}'.")
1358 else:
1359 releaseLevel = ReleaseLevel.Final
1361 version = cls(
1362 major=toInt(match["major"]),
1363 minor=toInt(match["minor"]),
1364 micro=toInt(match["micro"]),
1365 level=releaseLevel,
1366 number=toInt(match["number"]),
1367 post=toInt(match["post"]),
1368 dev=toInt(match["dev"]),
1369 epoch=toInt(match["epoch"]),
1370 build=toInt(match["build"]),
1371 postfix=match["postfix"],
1372 prefix=prefix if prefix != "" else None,
1373 # hash=match["hash"],
1374 flags=Flags.Clean
1375 )
1377 if validator is not None and not validator(version):
1378 raise VersionValidatorError(f"Failed to validate version string '{versionString}'.", version=version)
1380 return version
1382 @readonly
1383 def Patch(self) -> int:
1384 """
1385 Read-only property to access the patch number.
1387 The patch number is identical to the micro number.
1389 :returns: The patch number.
1390 """
1391 return self._micro
1393 def _equal(self, left: SemanticVersion, right: SemanticVersion) -> Nullable[bool]:
1394 """
1395 Private helper method to compute the equality of two :class:`SemanticVersion` instances.
1397 :param left: Left operand.
1398 :param right: Right operand.
1399 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1400 """
1401 return super()._equal(left, right)
1403 def _compare(self, left: SemanticVersion, right: SemanticVersion) -> Nullable[bool]:
1404 """
1405 Private helper method to compute the comparison of two :class:`SemanticVersion` instances.
1407 :param left: Left operand.
1408 :param right: Right operand.
1409 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1410 False if ``left`` is greater than ``right``. |br|
1411 Otherwise it's None (both operands are equal).
1412 """
1413 return super()._compare(left, right)
1415 def __eq__(self, other: Any) -> bool:
1416 """
1417 Compare two version numbers for equality.
1419 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1420 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1421 number is assumed (all other parts are zero).
1423 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1424 number.
1426 :param other: Operand to compare against.
1427 :returns: ``True``, if both version numbers are equal.
1428 :raises ValueError: If parameter ``other`` is None.
1429 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1430 """
1431 return super().__eq__(other)
1433 def __ne__(self, other: Any) -> bool:
1434 """
1435 Compare two version numbers for inequality.
1437 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1438 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1439 number is assumed (all other parts are zero).
1441 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1442 number.
1444 :param other: Operand to compare against.
1445 :returns: ``True``, if both version numbers are not equal.
1446 :raises ValueError: If parameter ``other`` is None.
1447 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1448 """
1449 return super().__ne__(other)
1451 def __lt__(self, other: Any) -> bool:
1452 """
1453 Compare two version numbers if the version is less than the second operand.
1455 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1456 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1457 number is assumed (all other parts are zero).
1459 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1460 number.
1462 :param other: Operand to compare against.
1463 :returns: ``True``, if version is less than the second operand.
1464 :raises ValueError: If parameter ``other`` is None.
1465 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1466 """
1467 return super().__lt__(other)
1469 def __le__(self, other: Any) -> bool:
1470 """
1471 Compare two version numbers if the version is less than or equal the second operand.
1473 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1474 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1475 number is assumed (all other parts are zero).
1477 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1478 number.
1480 :param other: Operand to compare against.
1481 :returns: ``True``, if version is less than or equal the second operand.
1482 :raises ValueError: If parameter ``other`` is None.
1483 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1484 """
1485 return super().__le__(other)
1487 def __gt__(self, other: Any) -> bool:
1488 """
1489 Compare two version numbers if the version is greater than the second operand.
1491 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1492 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1493 number is assumed (all other parts are zero).
1495 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1496 number.
1498 :param other: Operand to compare against.
1499 :returns: ``True``, if version is greater than the second operand.
1500 :raises ValueError: If parameter ``other`` is None.
1501 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1502 """
1503 return super().__gt__(other)
1505 def __ge__(self, other: Any) -> bool:
1506 """
1507 Compare two version numbers if the version is greater than or equal the second operand.
1509 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1510 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1511 number is assumed (all other parts are zero).
1513 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1514 number.
1516 :param other: Operand to compare against.
1517 :returns: ``True``, if version is greater than or equal the second operand.
1518 :raises ValueError: If parameter ``other`` is None.
1519 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1520 """
1521 return super().__ge__(other)
1523 def __rshift__(self, other: Union[SemanticVersion, str, int, None]) -> bool:
1524 """
1525 Return the minimum of this semantic version and a second operand.
1527 :param other: Second operand, a version, a version string or a major version number.
1528 :returns: ``True``, if this version is the minimum of both operands.
1529 :raises ValueError: If the second operand is ``None``.
1530 :raises TypeError: If the second operand is not a version, a string or an integer.
1531 """
1532 return super().__rshift__(other)
1534 def __hash__(self) -> int:
1535 """
1536 Compute a hash for this version number.
1538 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
1539 version unhashable.
1541 :returns: Hash of this version number.
1542 """
1543 return super().__hash__()
1545 def __format__(self, formatSpec: str) -> str:
1546 """
1547 Return a string representation of this version number according to the format specification.
1549 :param formatSpec: The format specification, using ``%``-placeholders for the version's parts.
1550 :returns: Formatted version number.
1551 :raises ValueError: If the format specification contains an unknown placeholder.
1552 """
1553 result = self._format(formatSpec)
1555 if (pos := result.find("%")) != -1 and result[pos + 1] != "%": # pragma: no cover
1556 raise ValueError(f"Unknown format specifier '%{result[pos + 1]}' in '{formatSpec}'.")
1558 return result.replace("%%", "%")
1560 def __repr__(self) -> str:
1561 """
1562 Return a normalized string representation of this version number.
1564 .. note::
1566 A prefix doesn't contribute to the version number's value, therefore it's not part of the normalized form. Use
1567 :meth:`__str__` to render a version number including its prefix.
1569 :returns: Raw version number representation without a prefix.
1570 """
1571 epoch = f"{self._epoch}{self._EPOCH_SEPARATOR}" if Parts.Epoch in self._parts else ""
1573 return f"{epoch}{self._major}.{self._minor}.{self._micro}"
1575 def __str__(self) -> str:
1576 """
1577 Return a string representation of this version number.
1579 :returns: Version number representation.
1580 """
1581 result = self._prefix if Parts.Prefix in self._parts else ""
1582 result += f"{self._epoch}{self._EPOCH_SEPARATOR}" if Parts.Epoch in self._parts else ""
1583 result += f"{self._major}" # major is always present
1584 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1585 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1586 result += f".{self._build}" if Parts.Build in self._parts else ""
1587 if self._releaseLevel is ReleaseLevel.Development:
1588 result += "-dev"
1589 elif self._releaseLevel is ReleaseLevel.Alpha:
1590 result += f".alpha{self._releaseNumber}"
1591 elif self._releaseLevel is ReleaseLevel.Beta:
1592 result += f".beta{self._releaseNumber}"
1593 elif self._releaseLevel is ReleaseLevel.Gamma: 1593 ↛ 1594line 1593 didn't jump to line 1594 because the condition on line 1593 was never true
1594 result += f".gamma{self._releaseNumber}"
1595 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate:
1596 result += f".rc{self._releaseNumber}"
1597 result += f".post{self._post}" if Parts.Post in self._parts else ""
1598 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1599 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1601 return result
1604@export
1605class PythonVersion(SemanticVersion):
1606 """
1607 Represents a Python version.
1608 """
1610 #: :pep:`440` writes an epoch ``v2!1.2.3``, where Debian and the default write ``2:1.2.3``.
1611 _EPOCH_SEPARATOR: ClassVar[str] = "!"
1613 @classmethod
1614 def FromSysVersionInfo(cls) -> PythonVersion:
1615 """
1616 Create a Python version from :data:`sys.version_info`.
1618 :returns: A PythonVersion instance of the current Python interpreter's version.
1619 :raises ToolingException: If the interpreter reports a release level this class doesn't know.
1620 """
1621 from sys import version_info
1623 if version_info.releaselevel == "final":
1624 rl = ReleaseLevel.Final
1625 number = None
1626 else: # pragma: no cover
1627 number = version_info.serial
1629 if version_info.releaselevel == "alpha":
1630 rl = ReleaseLevel.Alpha
1631 elif version_info.releaselevel == "beta":
1632 rl = ReleaseLevel.Beta
1633 elif version_info.releaselevel == "candidate":
1634 rl = ReleaseLevel.ReleaseCandidate
1635 else: # pragma: no cover
1636 raise ToolingException(f"Unsupported release level '{version_info.releaselevel}'.")
1638 return cls(version_info.major, version_info.minor, version_info.micro, level=rl, number=number)
1640 def __hash__(self) -> int:
1641 """
1642 Compute a hash for this version number.
1644 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
1645 version unhashable.
1647 :returns: Hash of this version number.
1648 """
1649 return super().__hash__()
1651 def __str__(self) -> str:
1652 """
1653 Return a string representation of this version number.
1655 :returns: Version number representation.
1656 """
1657 result = self._prefix if Parts.Prefix in self._parts else ""
1658 result += f"{self._epoch}{self._EPOCH_SEPARATOR}" if Parts.Epoch in self._parts else ""
1659 result += f"{self._major}" # major is always present
1660 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1661 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1662 if self._releaseLevel is ReleaseLevel.Alpha: 1662 ↛ 1663line 1662 didn't jump to line 1663 because the condition on line 1662 was never true
1663 result += f"a{self._releaseNumber}"
1664 elif self._releaseLevel is ReleaseLevel.Beta: 1664 ↛ 1665line 1664 didn't jump to line 1665 because the condition on line 1664 was never true
1665 result += f"b{self._releaseNumber}"
1666 elif self._releaseLevel is ReleaseLevel.Gamma: 1666 ↛ 1667line 1666 didn't jump to line 1667 because the condition on line 1666 was never true
1667 result += f"c{self._releaseNumber}"
1668 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate: 1668 ↛ 1669line 1668 didn't jump to line 1669 because the condition on line 1668 was never true
1669 result += f"rc{self._releaseNumber}"
1670 result += f".post{self._post}" if Parts.Post in self._parts else ""
1671 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1672 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1674 return result
1677@export
1678class CalendarVersion(Version):
1679 """Representation of a calendar version number like ``2021.10``."""
1681 _PARTCOUNT: ClassVar[int] = 3 #: Number of numeric parts a version number of this class can carry.
1683 _PATTERN: ClassVar[Pattern] = re_compile(
1684 r"^"
1685 r"(?P<prefix>rev|REV|[vViIrR])?"
1686 r"(?P<major>\d+)"
1687 r"(?:\.(?P<minor>\d+))?"
1688 r"(?:\.(?P<micro>\d+))?"
1689 r"$"
1690 ) #: Regular expression to parse a calendar version from a string.
1692 def __init__(
1693 self,
1694 major: int,
1695 minor: Nullable[int] = None,
1696 micro: Nullable[int] = None,
1697 build: Nullable[int] = None,
1698 flags: Flags = Flags.Clean,
1699 prefix: Nullable[str] = None,
1700 postfix: Nullable[str] = None
1701 ) -> None:
1702 """
1703 Initializes a calendar version number representation.
1705 :param major: Major number part of the version number.
1706 :param minor: Optional, minor number part of the version number.
1707 :param micro: Optional, micro (patch) number part of the version number.
1708 :param build: Optional, build number part of the version number.
1709 :param flags: Optional, the version number's flags.
1710 :param prefix: Optional, the version number's prefix.
1711 :param postfix: Optional, the version number's postfix.
1712 :raises TypeError: If parameter 'major' is not of type integer.
1713 :raises ValueError: If parameter 'major' is a negative number.
1714 :raises TypeError: If parameter 'minor' is not of type integer.
1715 :raises ValueError: If parameter 'minor' is a negative number.
1716 :raises TypeError: If parameter 'micro' is not of type integer.
1717 :raises ValueError: If parameter 'micro' is a negative number.
1718 :raises TypeError: If parameter 'build' is not of type integer.
1719 :raises ValueError: If parameter 'build' is a negative number.
1720 :raises TypeError: If parameter 'prefix' is not of type string.
1721 :raises TypeError: If parameter 'postfix' is not of type string.
1722 """
1723 super().__init__(major, minor, micro, build=build, postfix=postfix, prefix=prefix, flags=flags)
1725 @classmethod
1726 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[[CalendarVersion], bool]] = None) -> CalendarVersion:
1727 """
1728 Parse a version string and return a :class:`CalendarVersion` instance.
1730 Allowed prefix characters:
1732 * ``v|V`` - version, public version, public release
1733 * ``i|I`` - internal version, internal release
1734 * ``r|R`` - release, revision
1735 * ``rev|REV`` - revision
1737 A version number carries up to :attr:`_PARTCOUNT` numeric parts. :class:`YearMonthVersion`,
1738 :class:`YearWeekVersion` and :class:`YearReleaseVersion` describe two parts, so a third part is rejected for
1739 them.
1741 :param versionString: The version string to parse.
1742 :param validator: Optional, a validation function.
1743 :returns: An object representing a calendar version.
1744 :raises TypeError: If parameter ``versionString`` is not a string.
1745 :raises ValueError: If parameter ``versionString`` is None or empty.
1746 :raises ValueError: If parameter ``versionString`` isn't a calendar version number. |br|
1747 It may carry one of the prefixes ``v``, ``i``, ``r`` or ``rev``, e.g.
1748 ``v2024.04``.
1749 :raises ValueError: If parameter ``versionString`` has more parts than the class describes. |br|
1750 Use :class:`CalendarVersion` or :class:`YearMonthDayVersion` to parse a
1751 three-part calendar version number.
1752 :raises VersionValidatorError: If the parsed version is rejected by ``validator``.
1753 """
1754 if versionString is None:
1755 raise ValueError("Parameter 'versionString' is None.")
1756 elif not isinstance(versionString, str):
1757 ex = TypeError("Parameter 'versionString' is not of type 'str'.")
1758 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1759 raise ex
1760 elif (versionString := versionString.strip()) == "":
1761 raise ValueError("Parameter 'versionString' is empty.")
1763 if (match := cls._PATTERN.match(versionString)) is None:
1764 ex = ValueError(f"Syntax error in parameter 'versionString': '{versionString}'")
1765 ex.add_note(f"A calendar version number is made of up to {cls._PARTCOUNT} numeric parts, e.g. '2024.04'.")
1766 ex.add_note("It may carry one of the prefixes 'v', 'i', 'r' or 'rev', e.g. 'v2024.04'.")
1767 raise ex
1769 prefix = match["prefix"]
1770 minor = match["minor"]
1771 micro = match["micro"]
1773 if micro is not None and cls._PARTCOUNT < 3:
1774 ex = ValueError(f"Version number '{versionString}' has 3 parts, but '{cls.__name__}' describes {cls._PARTCOUNT}.")
1775 ex.add_note("Use 'CalendarVersion' or 'YearMonthDayVersion' to parse a 3-part calendar version number.")
1776 raise ex
1778 numbers = [int(match["major"])]
1779 if minor is not None:
1780 numbers.append(int(minor))
1781 if micro is not None:
1782 numbers.append(int(micro))
1784 version = cls(*numbers, flags=Flags.Clean, prefix=prefix if prefix != "" else None)
1786 if validator is not None and not validator(version):
1787 raise VersionValidatorError(f"Failed to validate version string '{versionString}'.", version=version)
1789 return version
1791 @readonly
1792 def Year(self) -> int:
1793 """
1794 Read-only property to access the year part.
1796 :returns: The year part.
1797 """
1798 return self._major
1800 def _equal(self, left: CalendarVersion, right: CalendarVersion) -> Nullable[bool]:
1801 """
1802 Private helper method to compute the equality of two :class:`CalendarVersion` instances.
1804 :param left: Left parameter.
1805 :param right: Right parameter.
1806 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1807 """
1808 return (left._major == right._major) and (left._minor == right._minor) and (left._micro == right._micro)
1810 def _compare(self, left: CalendarVersion, right: CalendarVersion) -> Nullable[bool]:
1811 """
1812 Private helper method to compute the comparison of two :class:`CalendarVersion` instances.
1814 :param left: Left parameter.
1815 :param right: Right parameter.
1816 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1817 False if ``left`` is greater than ``right``. |br|
1818 Otherwise it's None (both parameters are equal).
1819 """
1820 if left._major < right._major:
1821 return True
1822 elif left._major > right._major:
1823 return False
1825 if left._minor < right._minor:
1826 return True
1827 elif left._minor > right._minor:
1828 return False
1830 if left._micro < right._micro: 1830 ↛ 1831line 1830 didn't jump to line 1831 because the condition on line 1830 was never true
1831 return True
1832 elif left._micro > right._micro: 1832 ↛ 1833line 1832 didn't jump to line 1833 because the condition on line 1832 was never true
1833 return False
1835 return None
1837 def __eq__(self, other: Any) -> bool:
1838 """
1839 Compare two version numbers for equality.
1841 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1842 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1843 number is assumed (all other parts are zero).
1845 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1846 number.
1848 :param other: Parameter to compare against.
1849 :returns: ``True``, if both version numbers are equal.
1850 :raises ValueError: If parameter ``other`` is None.
1851 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
1852 """
1853 return super().__eq__(other)
1855 def __ne__(self, other: Any) -> bool:
1856 """
1857 Compare two version numbers for inequality.
1859 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1860 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1861 number is assumed (all other parts are zero).
1863 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1864 number.
1866 :param other: Parameter to compare against.
1867 :returns: ``True``, if both version numbers are not equal.
1868 :raises ValueError: If parameter ``other`` is None.
1869 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
1870 """
1871 return super().__ne__(other)
1873 def __lt__(self, other: Any) -> bool:
1874 """
1875 Compare two version numbers if the version is less than the second operand.
1877 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1878 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1879 number is assumed (all other parts are zero).
1881 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1882 number.
1884 :param other: Parameter to compare against.
1885 :returns: ``True``, if version is less than the second operand.
1886 :raises ValueError: If parameter ``other`` is None.
1887 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
1888 """
1889 return super().__lt__(other)
1891 def __le__(self, other: Any) -> bool:
1892 """
1893 Compare two version numbers if the version is less than or equal the second operand.
1895 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1896 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1897 number is assumed (all other parts are zero).
1899 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1900 number.
1902 :param other: Parameter to compare against.
1903 :returns: ``True``, if version is less than or equal the second operand.
1904 :raises ValueError: If parameter ``other`` is None.
1905 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
1906 """
1907 return super().__le__(other)
1909 def __gt__(self, other: Any) -> bool:
1910 """
1911 Compare two version numbers if the version is greater than the second operand.
1913 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1914 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1915 number is assumed (all other parts are zero).
1917 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1918 number.
1920 :param other: Parameter to compare against.
1921 :returns: ``True``, if version is greater than the second operand.
1922 :raises ValueError: If parameter ``other`` is None.
1923 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
1924 """
1925 return super().__gt__(other)
1927 def __ge__(self, other: Any) -> bool:
1928 """
1929 Compare two version numbers if the version is greater than or equal the second operand.
1931 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1932 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1933 number is assumed (all other parts are zero).
1935 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1936 number.
1938 :param other: Parameter to compare against.
1939 :returns: ``True``, if version is greater than or equal the second operand.
1940 :raises ValueError: If parameter ``other`` is None.
1941 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
1942 """
1943 return super().__ge__(other)
1945 def __hash__(self) -> int:
1946 """
1947 Compute a hash for this version number.
1949 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
1950 version unhashable.
1952 :returns: Hash of this version number.
1953 """
1954 return super().__hash__()
1956 def __format__(self, formatSpec: str) -> str:
1957 """
1958 Return a string representation of this version number according to the format specification.
1960 .. topic:: Format Specifiers
1962 * ``%M`` - major number (year)
1963 * ``%m`` - minor number (month/week)
1964 * ``%u`` - micro number (day)
1966 :param formatSpec: The format specification.
1967 :returns: Formatted version number.
1968 """
1969 if formatSpec == "":
1970 return self.__str__()
1972 result = formatSpec
1973 # result = result.replace("%P", str(self._prefix))
1974 result = result.replace("%M", str(self._major))
1975 result = result.replace("%m", str(self._minor))
1976 result = result.replace("%u", str(self._micro))
1977 # result = result.replace("%p", str(self._pre))
1979 return result.replace("%%", "%")
1981 def __repr__(self) -> str:
1982 """
1983 Return a normalized string representation of this version number.
1985 .. note::
1987 A prefix doesn't contribute to the version number's value, therefore it's not part of the normalized form. Use
1988 :meth:`__str__` to render a version number including its prefix.
1990 :returns: Raw version number representation without a prefix.
1991 """
1992 result = f"{self._major}.{self._minor}"
1993 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1995 return result
1997 def __str__(self) -> str:
1998 """
1999 Return a string representation of this version number with only the present parts.
2001 :returns: Version number representation including a prefix.
2002 """
2003 result = self._prefix if Parts.Prefix in self._parts else ""
2004 result += f"{self._major}"
2005 result += f".{self._minor}" if Parts.Minor in self._parts else ""
2006 result += f".{self._micro}" if Parts.Micro in self._parts else ""
2008 return result
2011@export
2012class YearMonthVersion(CalendarVersion):
2013 """Representation of a calendar version number made of year and month like ``2021.10``."""
2015 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and month.
2017 def __init__(
2018 self,
2019 year: int,
2020 month: Nullable[int] = None,
2021 build: Nullable[int] = None,
2022 flags: Flags = Flags.Clean,
2023 prefix: Nullable[str] = None,
2024 postfix: Nullable[str] = None
2025 ) -> None:
2026 """
2027 Initializes a year-month version number representation.
2029 :param year: Year part of the version number.
2030 :param month: Optional, month part of the version number.
2031 :param build: Optional, build number part of the version number.
2032 :param flags: Optional, the version number's flags.
2033 :param prefix: Optional, the version number's prefix.
2034 :param postfix: Optional, the version number's postfix.
2035 :raises TypeError: If parameter 'major' is not of type integer.
2036 :raises ValueError: If parameter 'major' is a negative number.
2037 :raises TypeError: If parameter 'minor' is not of type integer.
2038 :raises ValueError: If parameter 'minor' is a negative number.
2039 :raises TypeError: If parameter 'micro' is not of type integer.
2040 :raises ValueError: If parameter 'micro' is a negative number.
2041 :raises TypeError: If parameter 'build' is not of type integer.
2042 :raises ValueError: If parameter 'build' is a negative number.
2043 :raises TypeError: If parameter 'prefix' is not of type string.
2044 :raises TypeError: If parameter 'postfix' is not of type string.
2045 """
2046 super().__init__(year, month, None, build, flags, prefix, postfix)
2048 @readonly
2049 def Month(self) -> int:
2050 """
2051 Read-only property to access the month part.
2053 :returns: The month part.
2054 """
2055 return self._minor
2057 def __hash__(self) -> int:
2058 """
2059 Compute a hash for this version number.
2061 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
2062 version unhashable.
2064 :returns: Hash of this version number.
2065 """
2066 return super().__hash__()
2069@export
2070class YearWeekVersion(CalendarVersion):
2071 """Representation of a calendar version number made of year and week like ``2021.47``."""
2073 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and week.
2075 def __init__(
2076 self,
2077 year: int,
2078 week: Nullable[int] = None,
2079 build: Nullable[int] = None,
2080 flags: Flags = Flags.Clean,
2081 prefix: Nullable[str] = None,
2082 postfix: Nullable[str] = None
2083 ) -> None:
2084 """
2085 Initializes a year-week version number representation.
2087 :param year: Year part of the version number.
2088 :param week: Optional, week part of the version number.
2089 :param build: Optional, build number part of the version number.
2090 :param flags: Optional, the version number's flags.
2091 :param prefix: Optional, the version number's prefix.
2092 :param postfix: Optional, the version number's postfix.
2093 :raises TypeError: If parameter 'major' is not of type integer.
2094 :raises ValueError: If parameter 'major' is a negative number.
2095 :raises TypeError: If parameter 'minor' is not of type integer.
2096 :raises ValueError: If parameter 'minor' is a negative number.
2097 :raises TypeError: If parameter 'micro' is not of type integer.
2098 :raises ValueError: If parameter 'micro' is a negative number.
2099 :raises TypeError: If parameter 'build' is not of type integer.
2100 :raises ValueError: If parameter 'build' is a negative number.
2101 :raises TypeError: If parameter 'prefix' is not of type string.
2102 :raises TypeError: If parameter 'postfix' is not of type string.
2103 """
2104 super().__init__(year, week, None, build, flags, prefix, postfix)
2106 @readonly
2107 def Week(self) -> int:
2108 """
2109 Read-only property to access the week part.
2111 :returns: The week part.
2112 """
2113 return self._minor
2115 def __hash__(self) -> int:
2116 """
2117 Compute a hash for this version number.
2119 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
2120 version unhashable.
2122 :returns: Hash of this version number.
2123 """
2124 return super().__hash__()
2127@export
2128class YearReleaseVersion(CalendarVersion):
2129 """Representation of a calendar version number made of year and release per year like ``2021.2``."""
2131 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and release.
2133 def __init__(
2134 self,
2135 year: int,
2136 release: Nullable[int] = None,
2137 build: Nullable[int] = None,
2138 flags: Flags = Flags.Clean,
2139 prefix: Nullable[str] = None,
2140 postfix: Nullable[str] = None
2141 ) -> None:
2142 """
2143 Initializes a year-release version number representation.
2145 :param year: Year part of the version number.
2146 :param release: Optional, release number of the version number.
2147 :param build: Optional, build number part of the version number.
2148 :param flags: Optional, the version number's flags.
2149 :param prefix: Optional, the version number's prefix.
2150 :param postfix: Optional, the version number's postfix.
2151 :raises TypeError: If parameter 'major' is not of type integer.
2152 :raises ValueError: If parameter 'major' is a negative number.
2153 :raises TypeError: If parameter 'minor' is not of type integer.
2154 :raises ValueError: If parameter 'minor' is a negative number.
2155 :raises TypeError: If parameter 'micro' is not of type integer.
2156 :raises ValueError: If parameter 'micro' is a negative number.
2157 :raises TypeError: If parameter 'build' is not of type integer.
2158 :raises ValueError: If parameter 'build' is a negative number.
2159 :raises TypeError: If parameter 'prefix' is not of type string.
2160 :raises TypeError: If parameter 'postfix' is not of type string.
2161 """
2162 super().__init__(year, release, None, build, flags, prefix, postfix)
2164 @readonly
2165 def Release(self) -> int:
2166 """
2167 Read-only property to access the release number.
2169 :returns: The release number.
2170 """
2171 return self._minor
2173 def __hash__(self) -> int:
2174 """
2175 Compute a hash for this version number.
2177 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
2178 version unhashable.
2180 :returns: Hash of this version number.
2181 """
2182 return super().__hash__()
2185@export
2186class YearMonthDayVersion(CalendarVersion):
2187 """Representation of a calendar version number made of year, month and day like ``2021.10.15``."""
2189 def __init__(
2190 self,
2191 year: int,
2192 month: Nullable[int] = None,
2193 day: Nullable[int] = None,
2194 build: Nullable[int] = None,
2195 flags: Flags = Flags.Clean,
2196 prefix: Nullable[str] = None,
2197 postfix: Nullable[str] = None
2198 ) -> None:
2199 """
2200 Initializes a year-month-day version number representation.
2202 :param year: Year part of the version number.
2203 :param month: Optional, month part of the version number.
2204 :param day: Optional, day part of the version number.
2205 :param build: Optional, build number part of the version number.
2206 :param flags: Optional, the version number's flags.
2207 :param prefix: Optional, the version number's prefix.
2208 :param postfix: Optional, the version number's postfix.
2209 :raises TypeError: If parameter 'major' is not of type integer.
2210 :raises ValueError: If parameter 'major' is a negative number.
2211 :raises TypeError: If parameter 'minor' is not of type integer.
2212 :raises ValueError: If parameter 'minor' is a negative number.
2213 :raises TypeError: If parameter 'micro' is not of type integer.
2214 :raises ValueError: If parameter 'micro' is a negative number.
2215 :raises TypeError: If parameter 'build' is not of type integer.
2216 :raises ValueError: If parameter 'build' is a negative number.
2217 :raises TypeError: If parameter 'prefix' is not of type string.
2218 :raises TypeError: If parameter 'postfix' is not of type string.
2219 """
2220 super().__init__(year, month, day, build, flags, prefix, postfix)
2222 @readonly
2223 def Month(self) -> int:
2224 """
2225 Read-only property to access the month part.
2227 :returns: The month part.
2228 """
2229 return self._minor
2231 @readonly
2232 def Day(self) -> int:
2233 """
2234 Read-only property to access the day part.
2236 :returns: The day part.
2237 """
2238 return self._micro
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__()
2252V = TypeVar("V", bound=Version)
2254@export
2255class RangeBoundHandling(Flag):
2256 """
2257 A flag defining how to handle bounds in a range.
2259 If a bound is inclusive, the bound's value is within the range. If a bound is exclusive, the bound's value is the
2260 first value outside the range. Inclusive and exclusive behavior can be mixed for lower and upper bounds.
2261 """
2262 BothBoundsInclusive = 0 #: Lower and upper bound are inclusive.
2263 LowerBoundInclusive = 0 #: Lower bound is inclusive.
2264 UpperBoundInclusive = 0 #: Upper bound is inclusive.
2265 LowerBoundExclusive = 1 #: Lower bound is exclusive.
2266 UpperBoundExclusive = 2 #: Upper bound is exclusive.
2267 BothBoundsExclusive = 3 #: Lower and upper bound are exclusive.
2270@export
2271class VersionRange(Generic[V], metaclass=ExtendedType, slots=True):
2272 """
2273 Representation of a version range described by a lower bound and upper bound version.
2275 This version range works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
2277 A bound may be **unbound**, written as ``None``, meaning the range is open in that direction. That is how a
2278 dependency range like Maven's ``[1.0,)`` - *1.0 and everything after it* - is expressed, and it is what makes a
2279 single comparison such as ``>=1.0`` a range at all. A range unbound at both ends contains every version.
2281 Whether the bound itself belongs to the range is :class:`RangeBoundHandling`'s business, not the bound's, so
2282 each example names the comparison it stands for:
2284 .. code-block:: python
2286 VersionRange(SemanticVersion.Parse("1.0.0"), None) # >=1.0.0 ⟶ 1.0.0 and everything above it
2287 VersionRange(None, SemanticVersion.Parse("2.0.0")) # <=2.0.0 ⟶ everything up to 2.0.0
2288 VersionRange(None, None) # every version
2289 """
2290 _lowerBound: Nullable[V] #: Lower bound of the version range, or ``None`` if it is unbound.
2291 _upperBound: Nullable[V] #: Upper bound of the version range, or ``None`` if it is unbound.
2292 _boundHandling: RangeBoundHandling #: Strategy deciding whether the bounds are part of the range.
2294 def __init__(
2295 self,
2296 lowerBound: Nullable[V],
2297 upperBound: Nullable[V],
2298 boundHandling: RangeBoundHandling = RangeBoundHandling.BothBoundsInclusive
2299 ) -> None:
2300 """
2301 Initializes a version range described by a lower and upper bound.
2303 Either bound may be ``None``, which leaves the range open in that direction. The checks that relate the two
2304 bounds - that they are compatible types, and that the lower one isn't above the upper one - can only be made
2305 when both are present, so they are skipped for an open bound rather than failing on it.
2307 :param lowerBound: Lowest version (inclusive), or ``None`` to leave the range open downwards.
2308 :param upperBound: Highest version (inclusive), or ``None`` to leave the range open upwards.
2309 :param boundHandling: Optional, strategy deciding whether the bounds are part of the range.
2310 :raises TypeError: If parameter ``lowerBound`` is neither ``None`` nor of type :class:`Version`.
2311 :raises TypeError: If parameter ``upperBound`` is neither ``None`` nor of type :class:`Version`.
2312 :raises TypeError: If parameter ``lowerBound`` and ``upperBound`` are unrelated types.
2313 :raises ValueError: If parameter ``lowerBound`` isn't less than or equal to ``upperBound``.
2314 """
2315 if lowerBound is not None and not isinstance(lowerBound, Version):
2316 ex = TypeError("Parameter 'lowerBound' is not of type 'Version'.")
2317 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}'.")
2318 raise ex
2320 if upperBound is not None and not isinstance(upperBound, Version):
2321 ex = TypeError("Parameter 'upperBound' is not of type 'Version'.")
2322 ex.add_note(f"Got type '{getFullyQualifiedName(upperBound)}'.")
2323 raise ex
2325 if lowerBound is not None and upperBound is not None:
2326 if not self._AreCompatible(lowerBound, upperBound):
2327 ex = TypeError("Parameters 'lowerBound' and 'upperBound' are not compatible with each other.")
2328 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}' for lowerBound and "
2329 f"type '{getFullyQualifiedName(upperBound)}' for upperBound.")
2330 raise ex
2332 if not (lowerBound <= upperBound):
2333 ex = ValueError("Parameter 'lowerBound' isn't less than parameter 'upperBound'.")
2334 ex.add_note(f"Got '{lowerBound}' for lowerBound and '{upperBound}' for upperBound.")
2335 raise ex
2337 self._lowerBound = lowerBound
2338 self._upperBound = upperBound
2339 self._boundHandling = boundHandling
2341 @property
2342 def LowerBound(self) -> Nullable[V]:
2343 """
2344 Property to access the range's lower bound.
2346 Assigning ``None`` leaves the bound unbound, opening the range in that direction.
2348 :returns: Lower bound of the version range, or ``None`` if it is unbound.
2349 :raises TypeError: If an assigned value is neither ``None`` nor of type :class:`Version`.
2350 :raises TypeError: If an assigned value's type is unrelated to the range's upper bound.
2351 :raises ValueError: If an assigned value is above the range's upper bound. |br|
2352 The bounds are only related when both are present.
2353 """
2354 return self._lowerBound
2356 @LowerBound.setter
2357 def LowerBound(self, value: Nullable[V]) -> None:
2358 if value is not None and not isinstance(value, Version): 2358 ↛ 2359line 2358 didn't jump to line 2359 because the condition on line 2358 was never true
2359 ex = TypeError("Parameter 'value' is not of type 'Version'.")
2360 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2361 raise ex
2363 if value is not None and self._upperBound is not None:
2364 if not self._AreCompatible(value, self._upperBound):
2365 ex = TypeError("Parameter 'value' is not compatible with the range's upper bound.")
2366 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2367 ex.add_note(f"The upper bound is of type '{getFullyQualifiedName(self._upperBound)}'.")
2368 raise ex
2370 if not (value <= self._upperBound):
2371 ex = ValueError("Parameter 'value' isn't less than or equal to the range's upper bound.")
2372 ex.add_note(f"Got '{value}' for the lower bound; the upper bound is '{self._upperBound}'.")
2373 raise ex
2375 self._lowerBound = value
2377 @property
2378 def UpperBound(self) -> Nullable[V]:
2379 """
2380 Property to access the range's upper bound.
2382 Assigning ``None`` leaves the bound unbound, opening the range in that direction.
2384 :returns: Upper bound of the version range, or ``None`` if it is unbound.
2385 :raises TypeError: If an assigned value is neither ``None`` nor of type :class:`Version`.
2386 :raises TypeError: If an assigned value's type is unrelated to the range's lower bound.
2387 :raises ValueError: If an assigned value is below the range's lower bound. |br|
2388 The bounds are only related when both are present.
2389 """
2390 return self._upperBound
2392 @UpperBound.setter
2393 def UpperBound(self, value: Nullable[V]) -> None:
2394 if value is not None and not isinstance(value, Version): 2394 ↛ 2395line 2394 didn't jump to line 2395 because the condition on line 2394 was never true
2395 ex = TypeError("Parameter 'value' is not of type 'Version'.")
2396 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2397 raise ex
2399 if value is not None and self._lowerBound is not None:
2400 if not self._AreCompatible(value, self._lowerBound):
2401 ex = TypeError("Parameter 'value' is not compatible with the range's lower bound.")
2402 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2403 ex.add_note(f"The lower bound is of type '{getFullyQualifiedName(self._lowerBound)}'.")
2404 raise ex
2406 if not (self._lowerBound <= value):
2407 ex = ValueError("Parameter 'value' isn't greater than or equal to the range's lower bound.")
2408 ex.add_note(f"Got '{value}' for the upper bound; the lower bound is '{self._lowerBound}'.")
2409 raise ex
2411 self._upperBound = value
2413 @property
2414 def BoundHandling(self) -> RangeBoundHandling:
2415 """
2416 Property to access the range's bound handling strategy.
2418 :returns: The range's bound handling strategy.
2419 :raises TypeError: If an assigned value is not of type :class:`RangeBoundHandling`.
2420 """
2421 return self._boundHandling
2423 @BoundHandling.setter
2424 def BoundHandling(self, value: RangeBoundHandling) -> None:
2425 if not isinstance(value, RangeBoundHandling):
2426 ex = TypeError("Parameter 'value' is not of type 'RangeBoundHandling'.")
2427 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2428 raise ex
2430 self._boundHandling = value
2432 @staticmethod
2433 def _AreCompatible(left: Version, right: Version) -> bool:
2434 """
2435 Check whether two versions' types can be related to each other.
2437 Two versions relate when they are of the same class, or when one's class derives from the other's. So a
2438 :class:`SemanticVersion` relates to a :class:`PythonVersion`, which derives from it, and not to a
2439 :class:`CalendarVersion`, which is a sibling under :class:`Version`.
2441 This is the single rule every part of a range applies: to its two bounds against each other, to a version
2442 held against them, and to a bound assigned after construction.
2444 :param left: The first version.
2445 :param right: The second version.
2446 :returns: ``True``, if the two versions' types are related.
2447 """
2448 leftType = left.__class__
2449 rightType = right.__class__
2451 return leftType is rightType or issubclass(leftType, rightType) or issubclass(rightType, leftType)
2453 def _CheckCompatibility(self, other: Version) -> None:
2454 """
2455 Check that a version can be related to this range's bounds.
2457 The rule is the one :meth:`__init__` applies *between* the two bounds: the same class, or one deriving from
2458 the other. So a range bounded by :class:`SemanticVersion` accepts a :class:`PythonVersion`, because that
2459 derives from it, and refuses a :class:`CalendarVersion`, which is a sibling. A range whose bounds are both
2460 unbound carries no type to check against, so it accepts any version.
2462 :param other: The version to check against this range's bounds.
2463 :raises TypeError: If the version's type is unrelated to this range's bounds.
2464 """
2465 reference = self._lowerBound if self._lowerBound is not None else self._upperBound
2466 if reference is None:
2467 return
2469 if not self._AreCompatible(other, reference):
2470 ex = TypeError("Parameter 'other' is not compatible with version range.")
2471 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2472 ex.add_note(f"This range is bounded by type '{getFullyQualifiedName(reference)}'.")
2473 raise ex
2475 def __and__(self, other: Any) -> VersionRange[V]:
2476 """
2477 Compute the intersection of two version ranges.
2479 Each bound of the result comes from whichever range constrains it more tightly, and is inclusive only if it
2480 is inclusive in *that* range. Where both ranges name the same bound value, it is inclusive only when **both**
2481 include it - the intersection cannot admit a version one of its operands excludes.
2483 :param other: Second version range to intersect with.
2484 :returns: Intersected version range.
2485 :raises TypeError: If parameter 'other' is not of type :class:`VersionRange`.
2486 :raises TypeError: If the two ranges' bounds are of unrelated types.
2487 :raises ValueError: If intersection is empty.
2488 """
2489 if not isinstance(other, VersionRange): 2489 ↛ 2490line 2489 didn't jump to line 2490 because the condition on line 2489 was never true
2490 ex = TypeError("Parameter 'other' is not of type 'VersionRange'.")
2491 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2492 raise ex
2494 if self._lowerBound is not None and other._lowerBound is not None:
2495 if not self._AreCompatible(self._lowerBound, other._lowerBound): 2495 ↛ 2496line 2495 didn't jump to line 2496 because the condition on line 2495 was never true
2496 ex = TypeError("Parameter 'other's LowerBound and this range's 'LowerBound' are not compatible "
2497 "with each other.")
2498 ex.add_note(f"Got type '{getFullyQualifiedName(other._lowerBound)}' for other.LowerBound and "
2499 f"type '{getFullyQualifiedName(self._lowerBound)}' for self.LowerBound.")
2500 raise ex
2502 ownLowerExclusive = RangeBoundHandling.LowerBoundExclusive in self._boundHandling
2503 otherLowerExclusive = RangeBoundHandling.LowerBoundExclusive in other._boundHandling
2504 ownUpperExclusive = RangeBoundHandling.UpperBoundExclusive in self._boundHandling
2505 otherUpperExclusive = RangeBoundHandling.UpperBoundExclusive in other._boundHandling
2507 # An unbound lower end is the lowest of all, so the other range's bound wins; likewise the highest upper one.
2508 # Each bound keeps the handling of the range it came from; a shared value keeps the stricter of the two.
2509 if self._lowerBound is None:
2510 lBound = other._lowerBound
2511 lowerExclusive = otherLowerExclusive
2512 elif other._lowerBound is None:
2513 lBound = self._lowerBound
2514 lowerExclusive = ownLowerExclusive
2515 elif self._lowerBound > other._lowerBound:
2516 lBound = self._lowerBound
2517 lowerExclusive = ownLowerExclusive
2518 elif other._lowerBound > self._lowerBound:
2519 lBound = other._lowerBound
2520 lowerExclusive = otherLowerExclusive
2521 else:
2522 lBound = self._lowerBound
2523 lowerExclusive = ownLowerExclusive or otherLowerExclusive
2525 if self._upperBound is None:
2526 uBound = other._upperBound
2527 upperExclusive = otherUpperExclusive
2528 elif other._upperBound is None: 2528 ↛ 2529line 2528 didn't jump to line 2529 because the condition on line 2528 was never true
2529 uBound = self._upperBound
2530 upperExclusive = ownUpperExclusive
2531 elif self._upperBound < other._upperBound:
2532 uBound = self._upperBound
2533 upperExclusive = ownUpperExclusive
2534 elif other._upperBound < self._upperBound:
2535 uBound = other._upperBound
2536 upperExclusive = otherUpperExclusive
2537 else:
2538 uBound = self._upperBound
2539 upperExclusive = ownUpperExclusive or otherUpperExclusive
2541 if lBound is not None and uBound is not None and not (lBound <= uBound):
2542 ex = ValueError("The intersection of both version ranges is empty.")
2543 ex.add_note(f"Got value '{lBound}' for the highest lower bound.")
2544 ex.add_note(f"The lowest upper bound is '{uBound}'.")
2545 raise ex
2547 boundHandling = RangeBoundHandling.BothBoundsInclusive
2548 if lowerExclusive:
2549 boundHandling |= RangeBoundHandling.LowerBoundExclusive
2551 if upperExclusive:
2552 boundHandling |= RangeBoundHandling.UpperBoundExclusive
2554 return self.__class__(lBound, uBound, boundHandling)
2556 def __lt__(self, other: Any) -> bool:
2557 """
2558 Compare a version range and a version numbers if the version range is less than the second operand (version).
2560 :param other: Operand to compare against.
2561 :returns: ``True``, if version range is less than the second operand (version).
2562 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2563 """
2564 # TODO: support VersionRange < VersionRange too
2565 # TODO: support str, int, ... like Version ?
2566 if not isinstance(other, Version):
2567 ex = TypeError("Parameter 'other' is not of type 'Version'.")
2568 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2569 raise ex
2571 self._CheckCompatibility(other)
2573 if self._upperBound is None:
2574 return False
2576 return self._upperBound < other
2578 def __le__(self, other: Any) -> bool:
2579 """
2580 Compare a version range and a version numbers if the version range is less than or equal the second operand (version).
2582 :param other: Operand to compare against.
2583 :returns: ``True``, if version range is less than or equal the second operand (version).
2584 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2585 """
2586 # TODO: support VersionRange < VersionRange too
2587 # TODO: support str, int, ... like Version ?
2588 if not isinstance(other, Version): 2588 ↛ 2589line 2588 didn't jump to line 2589 because the condition on line 2588 was never true
2589 ex = TypeError("Parameter 'other' is not of type 'Version'.")
2590 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2591 raise ex
2593 self._CheckCompatibility(other)
2595 if self._upperBound is None:
2596 return False
2598 if RangeBoundHandling.UpperBoundExclusive in self._boundHandling:
2599 return self._upperBound < other
2601 return self._upperBound <= other
2603 def __gt__(self, other: Any) -> bool:
2604 """
2605 Compare a version range and a version numbers if the version range is greater than the second operand (version).
2607 :param other: Operand to compare against.
2608 :returns: ``True``, if version range is greater than the second operand (version).
2609 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2610 """
2611 # TODO: support VersionRange < VersionRange too
2612 # TODO: support str, int, ... like Version ?
2613 if not isinstance(other, Version): 2613 ↛ 2614line 2613 didn't jump to line 2614 because the condition on line 2613 was never true
2614 ex = TypeError("Parameter 'other' is not of type 'Version'.")
2615 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2616 raise ex
2618 self._CheckCompatibility(other)
2620 if self._lowerBound is None:
2621 return False
2623 return self._lowerBound > other
2625 def __ge__(self, other: Any) -> bool:
2626 """
2627 Compare a version range and a version numbers if the version range is greater than or equal the second operand (version).
2629 :param other: Operand to compare against.
2630 :returns: ``True``, if version range is greater than or equal the second operand (version).
2631 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2632 """
2633 # TODO: support VersionRange < VersionRange too
2634 # TODO: support str, int, ... like Version ?
2635 if not isinstance(other, Version): 2635 ↛ 2636line 2635 didn't jump to line 2636 because the condition on line 2635 was never true
2636 ex = TypeError("Parameter 'other' is not of type 'Version'.")
2637 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2638 raise ex
2640 self._CheckCompatibility(other)
2642 if self._lowerBound is None:
2643 return False
2645 if RangeBoundHandling.LowerBoundExclusive in self._boundHandling: 2645 ↛ 2646line 2645 didn't jump to line 2646 because the condition on line 2645 was never true
2646 return self._lowerBound > other
2648 return self._lowerBound >= other
2650 def __contains__(self, version: Version) -> bool:
2651 """
2652 Check if the version is in the version range.
2654 :param version: Optional, version to check.
2655 :returns: ``True``, if version is in range.
2656 :raises TypeError: If parameter ``version`` is not of type :class:`Version`.
2657 :raises TypeError: If parameter ``version``'s type is unrelated to this range's bounds. |br|
2658 The rule is the one :meth:`__init__` applies between the bounds.
2659 """
2660 if not isinstance(version, Version): 2660 ↛ 2661line 2660 didn't jump to line 2661 because the condition on line 2660 was never true
2661 ex = TypeError("Parameter 'item' is not of type 'Version'.")
2662 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
2663 raise ex
2665 self._CheckCompatibility(version)
2667 # An unbound end excludes nothing, so its half of the comparison is simply not made.
2668 if self._lowerBound is not None:
2669 if RangeBoundHandling.LowerBoundExclusive in self._boundHandling:
2670 if not (self._lowerBound < version):
2671 return False
2672 elif not (self._lowerBound <= version):
2673 return False
2675 if self._upperBound is not None:
2676 if RangeBoundHandling.UpperBoundExclusive in self._boundHandling:
2677 if not (version < self._upperBound):
2678 return False
2679 elif not (version <= self._upperBound):
2680 return False
2682 return True
2685@export
2686class VersionSet(Generic[V], metaclass=ExtendedType, slots=True):
2687 """
2688 Representation of an ordered set of versions.
2690 This version set works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
2691 """
2692 _items: list[V] #: An ordered list of set members.
2694 def __init__(self, versions: Union[Version, Iterable[V]]) -> None:
2695 """
2696 Initializes a version set either by a single version or an iterable of versions.
2698 :param versions: A single version or an iterable of versions.
2699 :raises ValueError: If parameter ``versions`` is None`.
2700 :raises TypeError: In case of a single version, if parameter ``version`` is not of type :class:`Version`.
2701 :raises TypeError: In case of an iterable, if parameter ``versions`` containes elements, which are not of type :class:`Version`.
2702 :raises TypeError: If parameter ``versions`` is neither a single version nor an iterable thereof.
2703 """
2704 if versions is None:
2705 raise ValueError("Parameter 'versions' is None.")
2707 if isinstance(versions, Version):
2708 self._items = [versions]
2709 elif isinstance(versions, abc_Iterable): 2709 ↛ 2727line 2709 didn't jump to line 2727 because the condition on line 2709 was always true
2710 iterator = iter(versions)
2711 try:
2712 firstVersion = next(iterator)
2713 except StopIteration:
2714 self._items = []
2715 return
2717 if not isinstance(firstVersion, Version): 2717 ↛ 2718line 2717 didn't jump to line 2718 because the condition on line 2717 was never true
2718 raise TypeError("First element in parameter 'versions' is not of type Version.")
2720 baseType = firstVersion.__class__
2721 for version in iterator:
2722 if not isinstance(version, baseType):
2723 raise TypeError(f"Element from parameter 'versions' is not of type {baseType.__name__}")
2725 self._items = list(sorted(versions))
2726 else:
2727 raise TypeError("Parameter 'versions' is not an Iterable.")
2729 def __and__(self, other: VersionSet[V]) -> VersionSet[V]:
2730 """
2731 Compute intersection of two version sets.
2733 :param other: Second set of versions.
2734 :returns: Intersection of two version sets.
2735 """
2736 selfIterator = self.__iter__()
2737 otherIterator = other.__iter__()
2739 result = []
2740 try:
2741 selfValue = next(selfIterator)
2742 otherValue = next(otherIterator)
2744 while True:
2745 if selfValue < otherValue:
2746 selfValue = next(selfIterator)
2747 elif otherValue < selfValue:
2748 otherValue = next(otherIterator)
2749 else:
2750 result.append(selfValue)
2751 selfValue = next(selfIterator)
2752 otherValue = next(otherIterator)
2754 except StopIteration:
2755 pass
2757 return VersionSet(result)
2759 def __or__(self, other: VersionSet[V]) -> VersionSet[V]:
2760 """
2761 Compute union of two version sets.
2763 :param other: Second set of versions.
2764 :returns: Union of two version sets.
2765 """
2766 selfIterator = self.__iter__()
2767 otherIterator = other.__iter__()
2769 result = []
2770 try:
2771 selfValue = next(selfIterator)
2772 except StopIteration:
2773 for otherValue in otherIterator:
2774 result.append(otherValue)
2776 try:
2777 otherValue = next(otherIterator)
2778 except StopIteration:
2779 for selfValue in selfIterator:
2780 result.append(selfValue)
2782 while True:
2783 if selfValue < otherValue:
2784 result.append(selfValue)
2785 try:
2786 selfValue = next(selfIterator)
2787 except StopIteration:
2788 result.append(otherValue)
2789 for otherValue in otherIterator: 2789 ↛ 2790line 2789 didn't jump to line 2790 because the loop on line 2789 never started
2790 result.append(otherValue)
2792 break
2793 elif otherValue < selfValue:
2794 result.append(otherValue)
2795 try:
2796 otherValue = next(otherIterator)
2797 except StopIteration:
2798 result.append(selfValue)
2799 for selfValue in selfIterator:
2800 result.append(selfValue)
2802 break
2803 else:
2804 result.append(selfValue)
2805 try:
2806 selfValue = next(selfIterator)
2807 except StopIteration:
2808 for otherValue in otherIterator: 2808 ↛ 2809line 2808 didn't jump to line 2809 because the loop on line 2808 never started
2809 result.append(otherValue)
2811 break
2813 try:
2814 otherValue = next(otherIterator)
2815 except StopIteration:
2816 for selfValue in selfIterator:
2817 result.append(selfValue)
2819 break
2821 return VersionSet(result)
2823 def __lt__(self, other: Any) -> bool:
2824 """
2825 Compare a version set and a version numbers if the version set is less than the second operand (version).
2827 :param other: Operand to compare against.
2828 :returns: ``True``, if version set is less than the second operand (version).
2829 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2830 """
2831 # TODO: support VersionRange < VersionRange too
2832 # TODO: support str, int, ... like Version ?
2833 if not isinstance(other, Version):
2834 ex = TypeError("Parameter 'other' is not of type 'Version'.")
2835 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2836 raise ex
2838 return self._items[-1] < other
2840 def __le__(self, other: Any) -> bool:
2841 """
2842 Compare a version set and a version numbers if the version set is less than or equal the second operand (version).
2844 :param other: Operand to compare against.
2845 :returns: ``True``, if version set is less than or equal the second operand (version).
2846 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2847 """
2848 # TODO: support VersionRange < VersionRange too
2849 # TODO: support str, int, ... like Version ?
2850 if not isinstance(other, Version): 2850 ↛ 2851line 2850 didn't jump to line 2851 because the condition on line 2850 was never true
2851 ex = TypeError("Parameter 'other' is not of type 'Version'.")
2852 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2853 raise ex
2855 return self._items[-1] <= other
2857 def __gt__(self, other: Any) -> bool:
2858 """
2859 Compare a version set and a version numbers if the version set is greater than the second operand (version).
2861 :param other: Operand to compare against.
2862 :returns: ``True``, if version set is greater than the second operand (version).
2863 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2864 """
2865 # TODO: support VersionRange < VersionRange too
2866 # TODO: support str, int, ... like Version ?
2867 if not isinstance(other, Version): 2867 ↛ 2868line 2867 didn't jump to line 2868 because the condition on line 2867 was never true
2868 ex = TypeError("Parameter 'other' is not of type 'Version'.")
2869 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2870 raise ex
2872 return self._items[0] > other
2874 def __ge__(self, other: Any) -> bool:
2875 """
2876 Compare a version set and a version numbers if the version set is greater than or equal the second operand (version).
2878 :param other: Operand to compare against.
2879 :returns: ``True``, if version set is greater than or equal the second operand (version).
2880 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2881 """
2882 # TODO: support VersionRange < VersionRange too
2883 # TODO: support str, int, ... like Version ?
2884 if not isinstance(other, Version): 2884 ↛ 2885line 2884 didn't jump to line 2885 because the condition on line 2884 was never true
2885 ex = TypeError("Parameter 'other' is not of type 'Version'.")
2886 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2887 raise ex
2889 return self._items[0] >= other
2891 def __contains__(self, version: V) -> bool:
2892 """
2893 Checks if the version a member of the set.
2895 :param version: Optional, the version to check.
2896 :returns: ``True``, if the version is a member of the set.
2897 """
2898 return version in self._items
2900 def __len__(self) -> int:
2901 """
2902 Returns the number of members in the set.
2904 :returns: Number of set members.
2905 """
2906 return len(self._items)
2908 def __iter__(self) -> Iterator[V]:
2909 """
2910 Returns an iterator to iterate all versions of this set from lowest to highest.
2912 :returns: Iterator to iterate versions.
2913 """
2914 return self._items.__iter__()
2916 def __getitem__(self, index: int) -> V:
2917 """
2918 Access to a version of a set by index.
2920 :param index: The index of the version to access.
2921 :returns: The indexed version.
2923 .. hint::
2925 Versions are ordered from lowest to highest version number.
2926 """
2927 return self._items[index]
2930#: The :class:`Version` class under a name no constraint shadows with a property of its own.
2931_VersionType = Version
2934@export
2935class VersionComparison(Enum):
2936 """
2937 The comparison one constraint of a :class:`VersionExpression` applies to a version.
2939 The six ordering comparisons mean the same thing in every packaging ecosystem, even where the spelling differs -
2940 Debian writes ``<<`` for :attr:`LessThan` and npm writes ``=`` for :attr:`Equal`. Each member's value is its
2941 *canonical* spelling, which is what a constraint renders as; a dialect maps its own spellings onto these members
2942 while parsing.
2943 """
2945 Equal = "==" #: The version has to be equal to the constraint's version.
2946 Unequal = "!=" #: The version must not be the constraint's version.
2947 LessThan = "<" #: The version has to be lower than the constraint's version.
2948 LessThanOrEqual = "<=" #: The version must not be higher than the constraint's version.
2949 GreaterThan = ">" #: The version has to be higher than the constraint's version.
2950 GreaterThanOrEqual = ">=" #: The version must not be lower than the constraint's version.
2951 CompatibleRelease = "~=" #: The version has to be compatible with the constraint's version (:pep:`440`).
2952 Caret = "^" #: The version must not change the constraint's leftmost non-zero part (npm).
2953 Tilde = "~" #: The version must not change the constraint's minor part (npm).
2955 def __str__(self) -> str:
2956 """
2957 Return the operator in its canonical spelling.
2959 :returns: The comparison's operator.
2960 """
2961 return self.value
2964@export
2965class VersionConstraint(Generic[V], metaclass=ExtendedType, slots=True):
2966 """
2967 One comparison of a :class:`VersionExpression`, such as ``>=1.2.0``.
2969 A constraint is a container of the versions satisfying it, so membership is asked with ``in``:
2971 .. code-block:: python
2973 constraint = VersionConstraint(VersionComparison.GreaterThanOrEqual, SemanticVersion.Parse("1.2.0")) # >=1.2.0
2974 SemanticVersion.Parse("1.5.0") in constraint # True
2976 .. seealso::
2978 :class:`CompatibleVersionConstraint`
2979 |rarr| The constraint implementing :attr:`~VersionComparison.CompatibleRelease`.
2980 """
2982 _comparison: VersionComparison #: The comparison this constraint applies.
2983 _version: V #: The version the compared version is held against.
2985 def __init__(self, comparison: VersionComparison, version: V) -> None:
2986 """
2987 Initialize a constraint from a comparison and the version it compares against.
2989 :param comparison: The comparison to apply.
2990 :param version: The version to compare against.
2991 :raises TypeError: If parameter 'comparison' is not of type :class:`VersionComparison`.
2992 :raises TypeError: If parameter 'version' is not of type :class:`Version`.
2993 :raises ValueError: If parameter 'comparison' is a shorthand like
2994 :attr:`~VersionComparison.CompatibleRelease`. |br|
2995 Use the matching :class:`RangeVersionConstraint`, which derives an upper bound from the
2996 version.
2997 """
2998 if not isinstance(comparison, VersionComparison):
2999 ex = TypeError("Parameter 'comparison' is not of type 'VersionComparison'.")
3000 ex.add_note(f"Got type '{getFullyQualifiedName(comparison)}'.")
3001 raise ex
3003 if not isinstance(version, Version):
3004 ex = TypeError("Parameter 'version' is not of type 'Version'.")
3005 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
3006 raise ex
3008 if comparison in _SHORTHAND_COMPARISONS and not isinstance(self, RangeVersionConstraint):
3009 ex = ValueError(f"Comparison '{comparison.name}' is not a plain comparison.")
3010 ex.add_note("Use the matching 'RangeVersionConstraint', which derives an upper bound from the version.")
3011 raise ex
3013 self._comparison = comparison
3014 self._version = version
3016 @readonly
3017 def Comparison(self) -> VersionComparison:
3018 """
3019 Read-only property to access the comparison this constraint applies (:attr:`_comparison`).
3021 :returns: The comparison.
3022 """
3023 return self._comparison
3025 @readonly
3026 def Version(self) -> V:
3027 """
3028 Read-only property to access the version this constraint compares against (:attr:`_version`).
3030 :returns: The version.
3031 """
3032 return self._version
3034 def __contains__(self, version: V) -> bool:
3035 """
3036 Check if a version satisfies this constraint.
3038 :param version: The version to check.
3039 :returns: ``True``, if the version satisfies the constraint.
3040 :raises TypeError: If parameter 'version' is not of type :class:`Version`.
3041 :raises ValueError: If this constraint carries a comparison a plain constraint cannot apply. |br|
3042 A shorthand is implemented by a :class:`RangeVersionConstraint`.
3043 """
3044 if not isinstance(version, Version): 3044 ↛ 3045line 3044 didn't jump to line 3045 because the condition on line 3044 was never true
3045 ex = TypeError("Parameter 'version' is not of type 'Version'.")
3046 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
3047 raise ex
3049 if self._comparison is VersionComparison.Equal:
3050 return version == self._version
3051 elif self._comparison is VersionComparison.Unequal:
3052 return version != self._version
3053 elif self._comparison is VersionComparison.LessThan:
3054 return version < self._version
3055 elif self._comparison is VersionComparison.LessThanOrEqual:
3056 return version <= self._version
3057 elif self._comparison is VersionComparison.GreaterThan:
3058 return version > self._version
3059 elif self._comparison is VersionComparison.GreaterThanOrEqual:
3060 return version >= self._version
3062 ex = ValueError(f"Comparison '{self._comparison.name}' cannot be applied by a plain constraint.")
3063 ex.add_note("A shorthand is implemented by a 'RangeVersionConstraint'.")
3064 raise ex
3066 def ToVersionRange(self) -> VersionRange[_VersionType]:
3067 """
3068 Convert this constraint into the :class:`VersionRange` it describes.
3070 Five of the six comparisons are intervals once a bound may be unbound:
3072 * ``>=1.2.0`` is ``[1.2.0, )``,
3073 * ``>1.2.0`` is ``(1.2.0, )``,
3074 * ``<=2.0.0`` is ``( , 2.0.0]``,
3075 * ``<2.0.0`` is ``( , 2.0.0)``,
3076 * ``==1.2.0`` is ``[1.2.0, 1.2.0]``, a range of exactly one version.
3078 :attr:`~VersionComparison.Unequal` is the exception and has no range: the complement of a single version is
3079 not an interval but a *union* of two, one below it and one above. That is why an expression keeps both
3080 representations rather than being replaced by a range.
3082 :returns: The range of versions satisfying this constraint.
3083 :raises ValueError: If this constraint is an :attr:`~VersionComparison.Unequal`, which no single range
3084 describes. |br|
3085 The complement of a version is a union of two intervals.
3086 """
3087 if self._comparison is VersionComparison.Equal:
3088 return VersionRange(self._version, self._version)
3089 elif self._comparison is VersionComparison.GreaterThanOrEqual:
3090 return VersionRange(self._version, None)
3091 elif self._comparison is VersionComparison.GreaterThan:
3092 return VersionRange(self._version, None, RangeBoundHandling.LowerBoundExclusive)
3093 elif self._comparison is VersionComparison.LessThanOrEqual:
3094 return VersionRange(None, self._version)
3095 elif self._comparison is VersionComparison.LessThan:
3096 return VersionRange(None, self._version, RangeBoundHandling.UpperBoundExclusive)
3098 ex = ValueError(f"Comparison '{self._comparison.name}' describes no single version range.")
3099 ex.add_note("The complement of a version is a union of two intervals, which a 'VersionRange' cannot hold.")
3100 raise ex
3102 def __str__(self) -> str:
3103 """
3104 Return the constraint in its canonical spelling.
3106 :returns: The operator followed by the version.
3107 """
3108 return f"{self._comparison}{self._version}"
3111#: The comparisons a plain :class:`VersionConstraint` cannot express, because each derives an upper bound.
3112_SHORTHAND_COMPARISONS = (
3113 VersionComparison.CompatibleRelease,
3114 VersionComparison.Caret,
3115 VersionComparison.Tilde,
3116)
3119@export
3120class RangeVersionConstraint(VersionConstraint[V]):
3121 """
3122 Base-class of the shorthand constraints meaning *at least this version, and below a derived bound*.
3124 Every packaging ecosystem has one of these, spells it differently, and derives its upper bound by a **different**
3125 rule:
3127 * :pep:`440` writes ``~=``,
3128 * npm writes ``^`` and ``~``,
3129 * RubyGems writes ``~>``.
3131 The rule is the only thing that differs, so it is what a derived class supplies in :meth:`_DeriveUpperBound`.
3133 .. seealso::
3135 :class:`CompatibleVersionConstraint`
3136 |rarr| :pep:`440`'s ``~=``.
3137 :class:`CaretVersionConstraint`
3138 |rarr| npm's ``^``.
3139 :class:`TildeVersionConstraint`
3140 |rarr| npm's ``~``.
3141 """
3143 _upperBound: SemanticVersion #: The first version outside the constraint, derived from the written version.
3145 def __init__(self, comparison: VersionComparison, version: V) -> None:
3146 """
3147 Initialize a shorthand constraint and derive its upper bound.
3149 :param comparison: The shorthand comparison this constraint applies.
3150 :param version: The version the shorthand is written with.
3151 :raises TypeError: If parameter 'version' is not of type :class:`SemanticVersion`. |br|
3152 An upper bound is derived from the version's parts, which only a semantic version has.
3153 :raises ValueError: If the version has too few parts for this shorthand.
3154 """
3155 if not isinstance(version, SemanticVersion):
3156 ex = TypeError("Parameter 'version' is not of type 'SemanticVersion'.")
3157 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
3158 raise ex
3160 super().__init__(comparison, version)
3162 self._upperBound = self._DeriveUpperBound(version)
3164 @abstractmethod
3165 def _DeriveUpperBound(self, version: SemanticVersion) -> SemanticVersion: # type: ignore[empty-body]
3166 """
3167 Derive the first version outside this constraint from the version it is written with.
3169 The bound has to be built in the written version's **epoch**. A bound left in epoch 0 outranks nothing, so
3170 the constraint would match no version at all - not even the one it was written with.
3172 :param version: The version the shorthand is written with, always a semantic version.
3173 :returns: The exclusive upper bound, in the same epoch as ``version``.
3174 :raises ValueError: If the version has too few parts for this shorthand.
3175 """
3177 @readonly
3178 def UpperBound(self) -> SemanticVersion:
3179 """
3180 Read-only property to access the first version outside this constraint (:attr:`_upperBound`).
3182 :returns: The exclusive upper bound derived from the written version.
3183 """
3184 return self._upperBound
3186 def __contains__(self, version: V) -> bool:
3187 """
3188 Check if a version is within this constraint.
3190 :param version: The version to check.
3191 :returns: ``True``, if the version is at least the written one and below the derived upper bound.
3192 :raises TypeError: If parameter 'version' is not of type :class:`Version`.
3193 """
3194 if not isinstance(version, Version): 3194 ↛ 3195line 3194 didn't jump to line 3195 because the condition on line 3194 was never true
3195 ex = TypeError("Parameter 'version' is not of type 'Version'.")
3196 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
3197 raise ex
3199 return self._version <= version < self._upperBound
3201 def ToVersionRange(self) -> VersionRange[_VersionType]:
3202 """
3203 Convert this constraint into the :class:`VersionRange` it describes.
3205 A shorthand constraint knows both of its bounds - the written version is the inclusive lower one, the derived
3206 bound the exclusive upper one - so the conversion is exact and loses nothing.
3208 :returns: The range of versions satisfying this constraint.
3209 """
3210 return VersionRange(
3211 self._version,
3212 self._upperBound,
3213 RangeBoundHandling.LowerBoundInclusive | RangeBoundHandling.UpperBoundExclusive
3214 )
3217@export
3218class CompatibleVersionConstraint(RangeVersionConstraint[V]):
3219 """
3220 The *compatible release* constraint, written ``~=`` by :pep:`440`.
3222 The last part written may move, the one to its left may not: ``~=1.2.3`` is ``<1.3``, ``~=1.2`` is ``<2`` and
3223 ``~=1.2.3.4`` is ``<1.2.4``. The written version therefore needs at least two parts - ``~=1`` would say nothing
3224 that ``>=1`` doesn't, and :pep:`440` rejects it for that reason.
3226 .. code-block:: python
3228 constraint = CompatibleVersionConstraint(PythonVersion.Parse("1.2.3"))
3229 PythonVersion.Parse("1.2.9") in constraint # True
3230 PythonVersion.Parse("1.3.0") in constraint # False
3231 """
3233 def __init__(self, version: V) -> None:
3234 """
3235 Initialize a compatible-release constraint from the version it is written with.
3237 :param version: The version to be compatible with, with at least two parts.
3238 """
3239 super().__init__(VersionComparison.CompatibleRelease, version)
3241 def _DeriveUpperBound(self, version: SemanticVersion) -> SemanticVersion:
3242 """
3243 Drop the last part written and increment the one that becomes the last.
3245 :param version: The version the shorthand is written with.
3246 :returns: The exclusive upper bound.
3247 :raises ValueError: If the version has fewer than two parts.
3248 """
3249 versionType = version.__class__
3250 epoch = version.Epoch if Parts.Epoch in version._parts else None
3251 if Parts.Build in version._parts:
3252 return versionType(version.Major, version.Minor, version.Patch + 1, epoch=epoch)
3253 elif Parts.Micro in version._parts:
3254 return versionType(version.Major, version.Minor + 1, epoch=epoch)
3255 elif Parts.Minor in version._parts:
3256 return versionType(version.Major + 1, epoch=epoch)
3258 ex = ValueError(f"Version '{version}' has too few parts for a compatible release.")
3259 ex.add_note("'~=1' would mean the same as '>=1'; write at least a major and a minor part.")
3260 raise ex
3263@export
3264class CaretVersionConstraint(RangeVersionConstraint[V]):
3265 """
3266 npm's ``^``: the version must not change the **leftmost non-zero** part of the one written.
3268 ``^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
3269 each part as breaking, which is what makes this different from :class:`TildeVersionConstraint`. A part that was
3270 not written cannot be the pivot, so ``^0`` is ``<1.0.0`` while ``^0.0`` is ``<0.1.0``.
3272 .. note::
3274 npm excludes pre-releases of the upper bound by writing it ``<2.0.0-0``. That distinction is not modelled
3275 here; a pre-release of the bound is compared by :class:`SemanticVersion`'s own ordering.
3276 """
3278 def __init__(self, version: V) -> None:
3279 """
3280 Initialize a caret constraint from the version it is written with.
3282 :param version: The version to be compatible with.
3283 """
3284 super().__init__(VersionComparison.Caret, version)
3286 def _DeriveUpperBound(self, version: SemanticVersion) -> SemanticVersion:
3287 """
3288 Increment the leftmost non-zero part that was actually written.
3290 :param version: The version the shorthand is written with.
3291 :returns: The exclusive upper bound.
3292 """
3293 versionType = version.__class__
3294 epoch = version.Epoch if Parts.Epoch in version._parts else None
3295 if version.Major != 0 or Parts.Minor not in version._parts:
3296 return versionType(version.Major + 1, epoch=epoch)
3297 elif version.Minor != 0 or Parts.Micro not in version._parts:
3298 return versionType(0, version.Minor + 1, epoch=epoch)
3300 return versionType(0, 0, version.Patch + 1, epoch=epoch)
3303@export
3304class TildeVersionConstraint(RangeVersionConstraint[V]):
3305 """
3306 npm's ``~``: the version must not change the minor part of the one written.
3308 ``~1.2.3`` and ``~1.2`` are both ``<1.3.0``. Only when no minor part was written does the major one become the
3309 pivot, so ``~1`` is ``<2.0.0``.
3311 This is **not** :pep:`440`'s ``~=``: the two agree on ``~1.2.3`` and disagree on ``~1.2``, which npm reads as
3312 ``<1.3.0`` and :pep:`440` as ``<2``.
3313 """
3315 def __init__(self, version: V) -> None:
3316 """
3317 Initialize a tilde constraint from the version it is written with.
3319 :param version: The version to be compatible with.
3320 """
3321 super().__init__(VersionComparison.Tilde, version)
3323 def _DeriveUpperBound(self, version: SemanticVersion) -> SemanticVersion:
3324 """
3325 Increment the minor part, or the major one when no minor part was written.
3327 :param version: The version the shorthand is written with.
3328 :returns: The exclusive upper bound.
3329 """
3330 versionType = version.__class__
3331 epoch = version.Epoch if Parts.Epoch in version._parts else None
3332 if Parts.Minor in version._parts:
3333 return versionType(version.Major, version.Minor + 1, epoch=epoch)
3335 return versionType(version.Major + 1, epoch=epoch)
3338@export
3339def _BuildConstraintPattern(
3340 operators: dict[str, VersionComparison],
3341 separators: str,
3342 versionType: type[Version]
3343) -> Pattern[str]:
3344 """
3345 Build the pattern matching one constraint of a :class:`VersionExpression` dialect.
3347 The operators are alternated longest-first, so ``>=`` wins over ``>`` and ``~=`` over any single character. The
3348 operator and its version are matched *together*, with whitespace allowed between them, which is what lets
3349 whitespace separate constraints without splitting ``>= 1.2.0`` in half.
3351 A version may hold none of the characters the dialect's operators are built from, and none of its separators, so
3352 those are excluded from it. Without that the optional operator group would let ``>=1.2.0 <2.0.0`` read its second
3353 constraint as the *version* ``<2.0.0``.
3355 The **epoch separator is the exception** and stays allowed: :pep:`440` writes an epoch ``1!1.0``, and ``!`` is
3356 also the first character of ``!=``. Excluding it would cut ``>=1!1.0`` short at the epoch. The operator
3357 alternation is tried before the version at every position, so ``!=`` is still read as an operator wherever a
3358 constraint can begin.
3360 This is a function rather than a method, so :class:`VersionExpression` can call it in its own class body. A
3361 dialect derived from it is served by :meth:`VersionExpression.__init_subclass__`.
3363 :param operators: The dialect's operator spellings.
3364 :param separators: The dialect's constraint separators.
3365 :param versionType: The dialect's version class, which names the epoch separator to keep.
3366 :returns: The pattern, with the operator as group 1 and the version as group 2.
3367 """
3368 alternation = "|".join(re_escape(operator) for operator in sorted(operators, key=len, reverse=True))
3369 excluded = (set("".join(operators)) | set(separators)) - set(versionType._EPOCH_SEPARATOR)
3371 # WORKAROUND: Python <3.12
3372 # Reusing the f-string's own quote character inside its expression needs PEP 701, so the escaped character
3373 # class is built into a variable first. On 3.11 the inlined form is a 'SyntaxError: f-string: unmatched (',
3374 # raised at import, which takes the whole package with it.
3375 # Replace by:
3376 # return re_compile(rf"({alternation})?\s*([^\s{re_escape("".join(sorted(excluded)))}]+)")
3377 excludedCharacters = re_escape("".join(sorted(excluded)))
3379 return re_compile(rf"({alternation})?\s*([^\s{excludedCharacters}]+)")
3382@export
3383class VersionExpression(Generic[V], metaclass=ExtendedType, slots=True):
3384 """
3385 A conjunction of :class:`VersionConstraint`\\ s, such as ``>=1.2.0,<2.0.0``.
3387 Every constraint has to be satisfied, which is what separating them means in every packaging ecosystem that has
3388 the notion. An expression with **no** constraints matches every version, so *no version restriction* can be
3389 represented rather than special-cased by its callers.
3391 .. code-block:: python
3393 expression = VersionExpression.Parse(">=1.2.0,<2.0.0")
3394 SemanticVersion.Parse("1.5.0") in expression # True
3395 SemanticVersion.Parse("2.0.0") in expression # False
3397 SemanticVersion.Parse("4.2.0") in VersionExpression.Parse("") # True - no constraints matches anything
3399 This class is the **ecosystem-neutral** dialect: the six ordering comparisons in their canonical spelling,
3400 separated by commas or whitespace. An ecosystem that spells its operators differently, or adds a shorthand,
3401 derives from it and overrides :attr:`_OPERATORS`, :attr:`_SEPARATORS`, :attr:`_VERSION_TYPE` or
3402 :attr:`_SHORTHANDS`. A dialect is data, not behavior.
3404 .. seealso::
3406 :class:`PythonVersionExpression`
3407 |rarr| The dialect :pep:`440` defines, which a Python requirement file writes.
3408 """
3410 #: Operator spellings this dialect accepts, mapped onto the comparison they mean.
3411 _OPERATORS: ClassVar[dict[str, VersionComparison]] = {
3412 "==": VersionComparison.Equal,
3413 "!=": VersionComparison.Unequal,
3414 "<=": VersionComparison.LessThanOrEqual,
3415 ">=": VersionComparison.GreaterThanOrEqual,
3416 "<": VersionComparison.LessThan,
3417 ">": VersionComparison.GreaterThan,
3418 }
3420 #: Characters separating one constraint from the next. Whitespace always separates as well.
3421 _SEPARATORS: ClassVar[str] = ","
3423 #: The :class:`Version` class this dialect parses its versions as, unless a caller names another.
3424 _VERSION_TYPE: ClassVar[type[Version]] = SemanticVersion
3426 #: The class each shorthand comparison is built as. A comparison absent here is a plain
3427 #: :class:`VersionConstraint`; the neutral dialect has no shorthand at all.
3428 _SHORTHANDS: ClassVar[dict[VersionComparison, type]] = {}
3430 #: This dialect's compiled constraint pattern, built from the three tables above. A derived dialect gets its own
3431 #: in :meth:`__init_subclass__`.
3432 _CONSTRAINT_PATTERN: ClassVar[Pattern[str]] = _BuildConstraintPattern(_OPERATORS, _SEPARATORS, _VERSION_TYPE)
3434 _constraints: tuple[VersionConstraint[V], ...] #: The constraints a version has to satisfy, all of them.
3436 def __init__(self, constraints: Iterable[VersionConstraint[V]] = ()) -> None:
3437 """
3438 Initialize an expression from its constraints.
3440 :param constraints: Optional, the constraints a version has to satisfy. None of them means *any version*.
3441 :raises ValueError: If parameter 'constraints' is None.
3442 :raises TypeError: If parameter 'constraints' is not iterable.
3443 :raises TypeError: If parameter 'constraints' contains an item that is not a :class:`VersionConstraint`.
3444 """
3445 if constraints is None:
3446 raise ValueError("Parameter 'constraints' is None.")
3447 elif not isinstance(constraints, abc_Iterable):
3448 ex = TypeError("Parameter 'constraints' is not iterable.")
3449 ex.add_note(f"Got type '{getFullyQualifiedName(constraints)}'.")
3450 raise ex
3452 items = tuple(constraints)
3453 for constraint in items:
3454 if not isinstance(constraint, VersionConstraint): 3454 ↛ 3455line 3454 didn't jump to line 3455 because the condition on line 3454 was never true
3455 ex = TypeError("Parameter 'constraints' contains an item that is not of type 'VersionConstraint'.")
3456 ex.add_note(f"Got type '{getFullyQualifiedName(constraint)}'.")
3457 raise ex
3459 self._constraints = items
3461 def __init_subclass__(cls, **kwargs: Any) -> None:
3462 """
3463 Compile the constraint pattern of a newly defined dialect.
3465 A dialect is data: its operators, its separators and its version type are class variables, so its pattern is
3466 settled once the class body has been read and is built here instead of on every :meth:`Parse`.
3468 :param kwargs: Keyword arguments passed on to the base implementation.
3469 """
3470 super().__init_subclass__(**kwargs)
3472 cls._CONSTRAINT_PATTERN = _BuildConstraintPattern(cls._OPERATORS, cls._SEPARATORS, cls._VERSION_TYPE)
3474 @classmethod
3475 def Parse(cls, expression: Nullable[str], versionType: Nullable[type[Version]] = None) -> Self:
3476 """
3477 Parse an expression such as ``>=1.2.0,<2.0.0`` into its constraints.
3479 A constraint without an operator is an equality, so ``1.2.0`` and ``==1.2.0`` are the same statement. An
3480 empty expression yields an expression with no constraints, which every version satisfies - that is how *no
3481 version restriction* is written.
3483 The expression is *scanned* rather than split, so a dialect separating constraints by whitespace does not
3484 break a constraint that has whitespace after its operator.
3486 :param expression: The expression to parse, or ``None`` for *any version*.
3487 :param versionType: Optional, the :class:`Version` class the versions are parsed as. Defaults to the
3488 dialect's :attr:`_VERSION_TYPE`.
3489 :returns: The parsed expression.
3490 :raises TypeError: If parameter 'expression' is not a string.
3491 :raises ValueError: If the expression holds input this dialect doesn't accept. |br|
3492 The note names the operators this dialect accepts.
3493 :raises ValueError: If a version in the expression can't be parsed. |br|
3494 The note names the operators this dialect accepts, because an operator another
3495 ecosystem spells differently is read as part of the version.
3496 """
3497 if expression is None:
3498 return cls()
3499 elif not isinstance(expression, str):
3500 ex = TypeError("Parameter 'expression' is not of type 'str'.")
3501 ex.add_note(f"Got type '{getFullyQualifiedName(expression)}'.")
3502 raise ex
3503 elif (expression := expression.strip()) == "":
3504 return cls()
3506 versionType = cls._VERSION_TYPE if versionType is None else versionType
3507 skippable = cls._SEPARATORS + " \t"
3508 # 'V' is unbound here - the version class comes from the dialect or the parameter, not from the type variable.
3509 constraints: list[VersionConstraint[Any]] = []
3510 position = 0
3512 for match in cls._CONSTRAINT_PATTERN.finditer(expression):
3513 if (skipped := expression[position:match.start()].strip(skippable)) != "":
3514 ex = ValueError(f"Expression '{expression}' has unexpected input at '{skipped}'.")
3515 ex.add_note(f"This dialect accepts the operators {', '.join(sorted(cls._OPERATORS))}.")
3516 raise ex
3518 operator = match.group(1)
3519 comparison = VersionComparison.Equal if operator is None else cls._OPERATORS[operator]
3520 try:
3521 version = versionType.Parse(match.group(2))
3522 except ValueError as cause:
3523 # An operator this dialect doesn't know is not recognized as one, so it lands in the version instead.
3524 ex = ValueError(f"Expression '{expression}' has unexpected input at '{match.group(0).strip()}'.")
3525 ex.add_note(f"This dialect accepts the operators {', '.join(sorted(cls._OPERATORS))}.")
3526 raise ex from cause
3528 if (shorthand := cls._SHORTHANDS.get(comparison, None)) is not None:
3529 constraints.append(shorthand(version))
3530 else:
3531 constraints.append(VersionConstraint(comparison, version))
3532 position = match.end()
3534 if (skipped := expression[position:].strip(skippable)) != "": 3534 ↛ 3535line 3534 didn't jump to line 3535 because the condition on line 3534 was never true
3535 ex = ValueError(f"Expression '{expression}' has unexpected input at '{skipped}'.")
3536 ex.add_note(f"This dialect accepts the operators {', '.join(sorted(cls._OPERATORS))}.")
3537 raise ex
3539 return cls(constraints)
3541 @readonly
3542 def Constraints(self) -> tuple[VersionConstraint[V], ...]:
3543 """
3544 Read-only property to access the constraints a version has to satisfy (:attr:`_constraints`).
3546 :returns: The constraints, empty if the expression matches every version.
3547 """
3548 return self._constraints
3550 @readonly
3551 def MatchesAnyVersion(self) -> bool:
3552 """
3553 Read-only property to return whether this expression constrains nothing.
3555 :returns: ``True``, if the expression has no constraints and every version satisfies it.
3556 """
3557 return len(self._constraints) == 0
3559 def __contains__(self, version: V) -> bool:
3560 """
3561 Check if a version satisfies every constraint of this expression.
3563 :param version: The version to check.
3564 :returns: ``True``, if the version satisfies all constraints. An expression without constraints is
3565 satisfied by every version.
3566 :raises TypeError: If parameter 'version' is not of type :class:`Version`.
3567 """
3568 if not isinstance(version, Version):
3569 ex = TypeError("Parameter 'version' is not of type 'Version'.")
3570 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
3571 raise ex
3573 return all(version in constraint for constraint in self._constraints)
3575 def ToVersionRange(self) -> VersionRange[Version]:
3576 """
3577 Convert this expression into the single :class:`VersionRange` it describes.
3579 An expression is a conjunction, so its range is the **intersection** of its constraints' ranges, which
3580 :meth:`VersionRange.__and__` computes. An expression with no constraints is the range unbound at both ends,
3581 since both match every version.
3583 Not every expression has a range. One containing an :attr:`~VersionComparison.Unequal` does not, because the
3584 complement of a version is a union of two intervals - ``>=1.0,!=1.3,<2.0`` is an ordinary requirement with
3585 no single range. That is why :class:`VersionExpression` is not replaced by :class:`VersionRange`: a range is
3586 one interval, an expression is any conjunction, and the second is strictly more expressive.
3588 :returns: The range of versions satisfying every constraint.
3589 :raises ValueError: If a constraint describes no range, which an :attr:`~VersionComparison.Unequal` never
3590 does.
3591 :raises ValueError: If the constraints have no version in common.
3592 """
3593 versionRange: VersionRange[_VersionType] = VersionRange(None, None)
3594 for constraint in self._constraints:
3595 versionRange = versionRange & constraint.ToVersionRange()
3597 return versionRange
3599 def __len__(self) -> int:
3600 """
3601 Return the number of constraints in this expression.
3603 :returns: Number of constraints.
3604 """
3605 return len(self._constraints)
3607 def __iter__(self) -> Iterator[VersionConstraint[V]]:
3608 """
3609 Iterate the constraints of this expression, in the order they were written.
3611 :returns: An iterator over the constraints.
3612 """
3613 return iter(self._constraints)
3615 def __str__(self) -> str:
3616 """
3617 Return the expression in this dialect's spelling.
3619 A :class:`VersionConstraint` renders itself canonically, which is not what every dialect writes - Debian
3620 spells :attr:`~VersionComparison.Equal` ``=`` and :attr:`~VersionComparison.LessThan` ``<<``. The dialect's
3621 own operator table answers what it writes; where a dialect has several spellings for one comparison, the
3622 first one wins.
3624 :returns: The constraints, joined by this dialect's separator, or an empty string if it constrains nothing.
3625 """
3626 separator: str = self._SEPARATORS[0] if len(self._SEPARATORS) > 0 else " "
3627 spelled: list[str] = []
3629 for constraint in self._constraints:
3630 for spelling, comparison in self._OPERATORS.items(): 3630 ↛ 3635line 3630 didn't jump to line 3635 because the loop on line 3630 didn't complete
3631 if comparison is constraint.Comparison:
3632 spelled.append(f"{spelling}{constraint.Version}")
3633 break
3634 else:
3635 spelled.append(str(constraint))
3637 return separator.join(spelled)
3640@export
3641class PythonVersionExpression(VersionExpression[V]):
3642 """
3643 A version expression in the dialect :pep:`440` defines, which is what a Python requirement file writes.
3645 It adds the compatible release operator ``~=`` to the six ordering comparisons and parses its versions as
3646 :class:`PythonVersion`:
3648 .. code-block:: python
3650 expression = PythonVersionExpression.Parse("~=1.2.3")
3651 PythonVersion.Parse("1.2.9") in expression # True
3652 PythonVersion.Parse("1.3.0") in expression # False
3654 .. seealso::
3656 :class:`CompatibleVersionConstraint`
3657 |rarr| What ``~=`` is parsed into, and how its upper bound is derived.
3658 """
3660 #: The neutral dialect's operators, plus the compatible release operator :pep:`440` defines.
3661 _OPERATORS: ClassVar[dict[str, VersionComparison]] = {
3662 **VersionExpression._OPERATORS,
3663 "~=": VersionComparison.CompatibleRelease,
3664 }
3666 #: :pep:`440` versions, so an epoch, a release candidate or a post-release parses.
3667 _VERSION_TYPE: ClassVar[type[Version]] = PythonVersion
3669 #: ``~=`` derives an upper bound, so it is not a plain comparison.
3670 _SHORTHANDS: ClassVar[dict[VersionComparison, type]] = {
3671 VersionComparison.CompatibleRelease: CompatibleVersionConstraint,
3672 }
3675@export
3676class NPMVersionExpression(VersionExpression[V]):
3677 """
3678 A version expression in npm's dialect, which is what a ``package.json`` dependency writes.
3680 npm differs from :pep:`440` in three ways that matter to a parser:
3682 * constraints are separated by **whitespace**, and a comma is a syntax error;
3683 * equality is written ``=``, never ``==``;
3684 * there is **no** ``!=`` - npm cannot exclude a single version this way.
3686 It adds ``^`` and ``~``, which are not :pep:`440`'s ``~=``:
3688 .. code-block:: python
3690 expression = NPMVersionExpression.Parse("^1.2.3")
3691 SemanticVersion.Parse("1.9.0") in expression # True
3692 SemanticVersion.Parse("2.0.0") in expression # False
3694 .. note::
3696 The shorthands ``1.2.x``, ``*``, the hyphen range ``1.2.3 - 2.3.4`` and the alternative ``||`` are **not**
3697 parsed. The first three are further rewriting rules; ``||`` is a disjunction, which this class cannot hold
3698 because every constraint of an expression has to be satisfied.
3700 .. seealso::
3702 :class:`CaretVersionConstraint` |br|
3703 :class:`TildeVersionConstraint`
3704 """
3706 #: npm's operators. No ``==`` and no ``!=``; ``^`` and ``~`` are npm's own shorthands.
3707 _OPERATORS: ClassVar[dict[str, VersionComparison]] = {
3708 "<=": VersionComparison.LessThanOrEqual,
3709 ">=": VersionComparison.GreaterThanOrEqual,
3710 "<": VersionComparison.LessThan,
3711 ">": VersionComparison.GreaterThan,
3712 "=": VersionComparison.Equal,
3713 "^": VersionComparison.Caret,
3714 "~": VersionComparison.Tilde,
3715 }
3717 #: npm separates constraints by whitespace alone; a comma is a syntax error there.
3718 _SEPARATORS: ClassVar[str] = ""
3720 #: npm is strict semantic versioning.
3721 _VERSION_TYPE: ClassVar[type[Version]] = SemanticVersion
3723 #: ``^`` and ``~`` each derive an upper bound, by different rules.
3724 _SHORTHANDS: ClassVar[dict[VersionComparison, type]] = {
3725 VersionComparison.Caret: CaretVersionConstraint,
3726 VersionComparison.Tilde: TildeVersionConstraint,
3727 }
3730@export
3731class DebianVersionExpression(VersionExpression[V]):
3732 """
3733 A version expression in Debian's dialect, as a ``debian/control`` dependency writes it inside its parentheses.
3735 Debian spells the strict comparisons ``<<`` and ``>>``, equality ``=``, and has **no** ``!=``. The obsolete
3736 spellings ``<`` and ``>`` are deliberately **not** accepted: ``dpkg`` still takes them but warns, because they
3737 historically meant ``<=`` and ``>=`` - reading them silently as the strict operators would invert their meaning.
3739 .. code-block:: python
3741 expression = DebianVersionExpression.Parse(">> 1.2.3")
3742 SemanticVersion.Parse("1.3.0") in expression # True
3744 .. note::
3746 A Debian dependency states **one** constraint per package mention - ``pkg (>= 1.0), pkg (<< 2.0)`` - so an
3747 expression here usually holds a single constraint. The comma is Debian's *dependency* separator, not a
3748 constraint separator.
3750 .. attention::
3752 Debian version strings are ``epoch:upstream-revision``. :class:`SemanticVersion` reads the revision as a
3753 postfix, but an **epoch** (``2:1.2.3-1``) does not parse, and ``1.2.3-1`` renders back as ``1.2.3+1``.
3754 Matching Debian versions faithfully needs a ``DebianVersion`` class, which pyTooling does not have.
3755 """
3757 #: Debian's operators. ``<<`` and ``>>`` are the strict ones; there is no ``!=``.
3758 _OPERATORS: ClassVar[dict[str, VersionComparison]] = {
3759 "<<": VersionComparison.LessThan,
3760 ">>": VersionComparison.GreaterThan,
3761 "<=": VersionComparison.LessThanOrEqual,
3762 ">=": VersionComparison.GreaterThanOrEqual,
3763 "=": VersionComparison.Equal,
3764 }
3766 #: Debian's comma separates dependencies rather than constraints, but accepting it costs nothing.
3767 _SEPARATORS: ClassVar[str] = ","
3769 #: The closest pyTooling has to a Debian version - see the class doc-string's caveat.
3770 _VERSION_TYPE: ClassVar[type[Version]] = SemanticVersion