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

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. 

33 

34.. hint:: 

35 

36 See :ref:`high-level help <VERSIONING>` for explanations and usage examples. 

37 

38.. seealso:: 

39 

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 

46 

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 

52 

53from pyTooling.Decorators import export, readonly 

54from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride 

55from pyTooling.Exceptions import ToolingException 

56from pyTooling.Common import getFullyQualifiedName 

57 

58 

59@export 

60class VersionValidatorError(ToolingException): 

61 """ 

62 Raised when a parsed version is rejected by the validator it was parsed with. 

63 

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 """ 

68 

69 _version: Nullable[Version] #: The version rejected by a validator. 

70 

71 def __init__(self, message: str, /, *, version: Nullable[Version] = None) -> None: 

72 """ 

73 Initializes the exception with the rejected version. 

74 

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 

80 

81 @readonly 

82 def Version(self) -> Nullable[Version]: 

83 """ 

84 Read-only property to access the version the validator rejected (:attr:`_version`). 

85 

86 :returns: The rejected version, or ``None`` if it wasn't recorded. 

87 """ 

88 return self._version 

89 

90 

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 

112 

113 

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 #: 

123 

124 def __eq__(self, other: Any) -> bool: 

125 """ 

126 Compare two release levels if the level is equal to the second operand. 

127 

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) 

134 

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 

140 

141 return self is other 

142 

143 def __ne__(self, other: Any) -> bool: 

144 """ 

145 Compare two release levels if the level is unequal to the second operand. 

146 

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) 

153 

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 

159 

160 return self is not other 

161 

162 def __lt__(self, other: Any) -> bool: 

163 """ 

164 Compare two release levels if the level is less than the second operand. 

165 

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) 

172 

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 

178 

179 return self.value < other.value 

180 

181 def __le__(self, other: Any) -> bool: 

182 """ 

183 Compare two release levels if the level is less than or equal the second operand. 

184 

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) 

191 

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 

197 

198 return self.value <= other.value 

199 

200 def __gt__(self, other: Any) -> bool: 

201 """ 

202 Compare two release levels if the level is greater than the second operand. 

203 

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) 

210 

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 

216 

217 return self.value > other.value 

218 

219 def __ge__(self, other: Any) -> bool: 

220 """ 

221 Compare two release levels if the level is greater than or equal the second operand. 

222 

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) 

229 

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 

235 

236 return self.value >= other.value 

237 

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. 

241 

242 The hash is derived from the release level's value, so two release levels compare and hash alike. 

243 

244 :returns: Hash of the release level's value. 

245 """ 

246 return hash(self.value) 

247 

248 def __str__(self) -> str: 

249 """ 

250 Returns the release level's string equivalent. 

251 

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" 

265 

266 raise ToolingException(f"Unknown ReleaseLevel '{self.name}'.") 

267 

268 

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. 

275 

276 CVS = 16 #: Concurrent Versions System (CVS) 

277 SVN = 32 #: Subversion (SVN) 

278 Git = 64 #: Git 

279 Hg = 128 #: Mercurial (Hg) 

280 

281 

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. 

292 

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 

303 

304 if majorBits is not None: 

305 majorMax = 2**majorBits - 1 

306 

307 if minorBits is not None: 

308 minorMax = 2**minorBits - 1 

309 

310 if microBits is not None: 

311 microMax = 2 ** microBits - 1 

312 

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 

315 

316 def validator(version: SemanticVersion) -> bool: 

317 """ 

318 Validator function, which checks each version part against the maximum its word size allows. 

319 

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}.") 

326 

327 if Parts.Minor in version._parts and version._minor > minorMax: 

328 raise ValueError(f"Field 'Version.Minor' > {minorMax}.") 

329 

330 if Parts.Micro in version._parts and version._micro > microMax: 

331 raise ValueError(f"Field 'Version.Micro' > {microMax}.") 

332 

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}.") 

335 

336 return True 

337 

338 return validator 

339 

340 

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]. 

351 

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 

361 

362 def validator(version: SemanticVersion) -> bool: 

363 """ 

364 Validator function, which checks each version part against its maximum value. 

365 

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}.") 

372 

373 if Parts.Minor in version._parts and version._minor > minorMax: 

374 raise ValueError(f"Field 'Version.Minor' > {minorMax}.") 

375 

376 if Parts.Micro in version._parts and version._micro > microMax: 

377 raise ValueError(f"Field 'Version.Micro' > {microMax}.") 

378 

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}.") 

381 

382 return True 

383 

384 return validator 

385 

386 

387@export 

388class Version(metaclass=ExtendedType, slots=True): 

389 """Base-class for a version representation.""" 

390 

391 __hash: Nullable[int] #: once computed hash of the object 

392 

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] = ":" 

396 

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. 

411 

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. 

431 

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 

459 

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.") 

466 

467 self._parts = Parts.Major 

468 self._major = major 

469 

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.") 

477 

478 self._parts |= Parts.Epoch 

479 self._epoch = epoch 

480 else: 

481 self._epoch = 0 

482 

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.") 

490 

491 self._parts |= Parts.Minor 

492 self._minor = minor 

493 else: 

494 self._minor = 0 

495 

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.") 

503 

504 self._parts |= Parts.Micro 

505 self._micro = micro 

506 else: 

507 self._micro = 0 

508 

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'.") 

518 

519 self._parts |= Parts.Level 

520 self._releaseLevel = level 

521 self._releaseNumber = 0 

522 else: 

523 self._parts |= Parts.Level 

524 self._releaseLevel = level 

525 

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.") 

533 

534 self._releaseNumber = number 

535 else: 

536 self._releaseNumber = 0 

537 

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.") 

545 

546 self._parts |= Parts.Dev 

547 self._dev = dev 

548 else: 

549 self._dev = 0 

550 

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.") 

558 

559 self._parts |= Parts.Post 

560 self._post = post 

561 else: 

562 self._post = 0 

563 

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.") 

571 

572 self._build = build 

573 self._parts |= Parts.Build 

574 else: 

575 self._build = 0 

576 

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 

582 

583 self._parts |= Parts.Postfix 

584 self._postfix = postfix 

585 else: 

586 self._postfix = "" 

587 

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 

593 

594 self._parts |= Parts.Prefix 

595 self._prefix = prefix 

596 else: 

597 self._prefix = "" 

598 

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 

604 

605 self._parts |= Parts.Hash 

606 self._hash = hash 

607 else: 

608 self._hash = "" 

609 

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 

616 

617 self._flags = flags 

618 

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. 

624 

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 """ 

629 

630 @readonly 

631 def Parts(self) -> Parts: 

632 """ 

633 Read-only property to access the used parts of this version number. 

634 

635 :returns: A flag enumeration of used version number parts. 

636 """ 

637 return self._parts 

638 

639 @readonly 

640 def Prefix(self) -> str: 

641 """ 

642 Read-only property to access the version number's prefix. 

643 

644 :returns: The prefix of the version number. 

645 """ 

646 return self._prefix 

647 

648 @readonly 

649 def Epoch(self) -> int: 

650 """ 

651 Read-only property to access the epoch (:attr:`_epoch`). 

652 

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. 

657 

658 :returns: The epoch, or ``0`` if the version carries none. 

659 """ 

660 return self._epoch 

661 

662 @readonly 

663 def Major(self) -> int: 

664 """ 

665 Read-only property to access the major number. 

666 

667 :returns: The major number. 

668 """ 

669 return self._major 

670 

671 @readonly 

672 def Minor(self) -> int: 

673 """ 

674 Read-only property to access the minor number. 

675 

676 :returns: The minor number. 

677 """ 

678 return self._minor 

679 

680 @readonly 

681 def Micro(self) -> int: 

682 """ 

683 Read-only property to access the micro number. 

684 

685 :returns: The micro number. 

686 """ 

687 return self._micro 

688 

689 @readonly 

690 def ReleaseLevel(self) -> ReleaseLevel: 

691 """ 

692 Read-only property to access the release level. 

693 

694 :returns: The release level. 

695 """ 

696 return self._releaseLevel 

697 

698 @readonly 

699 def ReleaseNumber(self) -> int: 

700 """ 

701 Read-only property to access the release number. 

702 

703 :returns: The release number. 

704 """ 

705 return self._releaseNumber 

706 

707 @readonly 

708 def Post(self) -> int: 

709 """ 

710 Read-only property to access the post number. 

711 

712 :returns: The post number. 

713 """ 

714 return self._post 

715 

716 @readonly 

717 def Dev(self) -> int: 

718 """ 

719 Read-only property to access the development number. 

720 

721 :returns: The development number. 

722 """ 

723 return self._dev 

724 

725 @readonly 

726 def Build(self) -> int: 

727 """ 

728 Read-only property to access the build number. 

729 

730 :returns: The build number. 

731 """ 

732 return self._build 

733 

734 @readonly 

735 def Postfix(self) -> str: 

736 """ 

737 Read-only property to access the version number's postfix. 

738 

739 :returns: The postfix of the version number. 

740 """ 

741 return self._postfix 

742 

743 @readonly 

744 def Hash(self) -> str: 

745 """ 

746 Read-only property to access the version number's hash. 

747 

748 :returns: The hash. 

749 """ 

750 return self._hash 

751 

752 @readonly 

753 def Flags(self) -> Flags: 

754 """ 

755 Read-only property to access the version number's flags. 

756 

757 :returns: The flags of the version number. 

758 """ 

759 return self._flags 

760 

761 def _equal(self, left: Version, right: Version) -> Nullable[bool]: 

762 """ 

763 Private helper method to compute the equality of two :class:`Version` instances. 

764 

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 ) 

781 

782 def _compare(self, left: Version, right: Version) -> Nullable[bool]: 

783 """ 

784 Private helper method to compute the comparison of two :class:`Version` instances. 

785 

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 

796 

797 if left._major < right._major: 

798 return True 

799 elif left._major > right._major: 

800 return False 

801 

802 if left._minor < right._minor: 

803 return True 

804 elif left._minor > right._minor: 

805 return False 

806 

807 if left._micro < right._micro: 

808 return True 

809 elif left._micro > right._micro: 

810 return False 

811 

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 

816 

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 

821 

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 

826 

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 

831 

832 if left._build < right._build: 

833 return True 

834 elif left._build > right._build: 

835 return False 

836 

837 return None 

838 

839 def _minimum(self, actual: Version, expected: Version) -> Nullable[bool]: 

840 """ 

841 Check if a version fulfills a minimum requirement. 

842 

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. 

845 

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 

852 

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 

857 

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 

862 

863 if Parts.Micro in expected._parts: 

864 return actual._micro >= expected._micro 

865 

866 return True 

867 

868 def _format(self, formatSpec: str) -> str: 

869 """ 

870 Return a string representation of this version number according to the format specification. 

871 

872 .. topic:: Format Specifiers 

873 

874 * ``%p`` - prefix 

875 * ``%M`` - major number 

876 * ``%m`` - minor number 

877 * ``%u`` - micro number 

878 * ``%b`` - build number 

879 

880 :param formatSpec: The format specification. 

881 :returns: Formatted version number. 

882 """ 

883 if formatSpec == "": 

884 return self.__str__() 

885 

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)) 

897 

898 return result 

899 

900 @mustoverride 

901 def __eq__(self, other: Any) -> bool: 

902 """ 

903 Compare two version numbers for equality. 

904 

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). 

908 

909 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

910 number. 

911 

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 

930 

931 return self._equal(self, other) 

932 

933 @mustoverride 

934 def __ne__(self, other: Any) -> bool: 

935 """ 

936 Compare two version numbers for inequality. 

937 

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). 

941 

942 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

943 number. 

944 

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 

963 

964 return not self._equal(self, other) 

965 

966 @mustoverride 

967 def __lt__(self, other: Any) -> bool: 

968 """ 

969 Compare two version numbers if the version is less than the second operand. 

970 

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). 

975 

976 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

977 number. 

978 

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 

1002 

1003 return self._compare(self, other) is True 

1004 

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. 

1009 

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). 

1014 

1015 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1016 number. 

1017 

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 

1043 

1044 result = self._compare(self, other) 

1045 return result if result is not None else equalValue 

1046 

1047 @mustoverride 

1048 def __gt__(self, other: Any) -> bool: 

1049 """ 

1050 Compare two version numbers if the version is greater than the second operand. 

1051 

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). 

1056 

1057 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1058 number. 

1059 

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 

1083 

1084 return self._compare(self, other) is False 

1085 

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. 

1090 

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). 

1095 

1096 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1097 number. 

1098 

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 

1124 

1125 result = self._compare(self, other) 

1126 return not result if result is not None else equalValue 

1127 

1128 def __rshift__(self, other: Union[Version, str, int, None]) -> bool: 

1129 """ 

1130 Return the minimum of this version and a second operand. 

1131 

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 

1150 

1151 return self._minimum(self, other) 

1152 

1153 def __hash__(self) -> int: 

1154 """ 

1155 Compute a hash for this version number and cache it. 

1156 

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. 

1159 

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 

1179 

1180 

1181@export 

1182class SemanticVersion(Version): 

1183 """Representation of a semantic version number like ``3.7.12``.""" 

1184 

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 

1206 

1207 def __init_subclass__(cls, **kwargs: Any) -> None: 

1208 """ 

1209 Rebuild the pattern when a derived class spells the epoch separator differently. 

1210 

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. 

1214 

1215 :param kwargs: Keyword arguments passed on to the base implementation. 

1216 """ 

1217 super().__init_subclass__(**kwargs) 

1218 

1219 if "_PATTERN" in cls.__dict__ or cls._EPOCH_SEPARATOR == SemanticVersion._EPOCH_SEPARATOR: 

1220 return 

1221 

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 )) 

1226 

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. 

1246 

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) 

1279 

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. 

1284 

1285 Allowed prefix characters: 

1286 

1287 * ``v|V`` - version, public version, public release 

1288 * ``i|I`` - internal version, internal release 

1289 * ``r|R`` - release, revision 

1290 * ``rev|REV`` - revision 

1291 

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.") 

1312 

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 

1317 

1318 def toInt(value: Nullable[str]) -> Nullable[int]: 

1319 """ 

1320 Nested function converting an optional part of a version string to an integer. 

1321 

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 

1328 

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 

1333 

1334 prefix = match["prefix"] 

1335 

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 

1360 

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 ) 

1376 

1377 if validator is not None and not validator(version): 

1378 raise VersionValidatorError(f"Failed to validate version string '{versionString}'.", version=version) 

1379 

1380 return version 

1381 

1382 @readonly 

1383 def Patch(self) -> int: 

1384 """ 

1385 Read-only property to access the patch number. 

1386 

1387 The patch number is identical to the micro number. 

1388 

1389 :returns: The patch number. 

1390 """ 

1391 return self._micro 

1392 

1393 def _equal(self, left: SemanticVersion, right: SemanticVersion) -> Nullable[bool]: 

1394 """ 

1395 Private helper method to compute the equality of two :class:`SemanticVersion` instances. 

1396 

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) 

1402 

1403 def _compare(self, left: SemanticVersion, right: SemanticVersion) -> Nullable[bool]: 

1404 """ 

1405 Private helper method to compute the comparison of two :class:`SemanticVersion` instances. 

1406 

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) 

1414 

1415 def __eq__(self, other: Any) -> bool: 

1416 """ 

1417 Compare two version numbers for equality. 

1418 

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). 

1422 

1423 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1424 number. 

1425 

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) 

1432 

1433 def __ne__(self, other: Any) -> bool: 

1434 """ 

1435 Compare two version numbers for inequality. 

1436 

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). 

1440 

1441 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1442 number. 

1443 

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) 

1450 

1451 def __lt__(self, other: Any) -> bool: 

1452 """ 

1453 Compare two version numbers if the version is less than the second operand. 

1454 

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). 

1458 

1459 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1460 number. 

1461 

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) 

1468 

1469 def __le__(self, other: Any) -> bool: 

1470 """ 

1471 Compare two version numbers if the version is less than or equal the second operand. 

1472 

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). 

1476 

1477 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1478 number. 

1479 

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) 

1486 

1487 def __gt__(self, other: Any) -> bool: 

1488 """ 

1489 Compare two version numbers if the version is greater than the second operand. 

1490 

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). 

1494 

1495 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1496 number. 

1497 

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) 

1504 

1505 def __ge__(self, other: Any) -> bool: 

1506 """ 

1507 Compare two version numbers if the version is greater than or equal the second operand. 

1508 

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). 

1512 

1513 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1514 number. 

1515 

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) 

1522 

1523 def __rshift__(self, other: Union[SemanticVersion, str, int, None]) -> bool: 

1524 """ 

1525 Return the minimum of this semantic version and a second operand. 

1526 

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) 

1533 

1534 def __hash__(self) -> int: 

1535 """ 

1536 Compute a hash for this version number. 

1537 

1538 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the 

1539 version unhashable. 

1540 

1541 :returns: Hash of this version number. 

1542 """ 

1543 return super().__hash__() 

1544 

1545 def __format__(self, formatSpec: str) -> str: 

1546 """ 

1547 Return a string representation of this version number according to the format specification. 

1548 

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) 

1554 

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}'.") 

1557 

1558 return result.replace("%%", "%") 

1559 

1560 def __repr__(self) -> str: 

1561 """ 

1562 Return a normalized string representation of this version number. 

1563 

1564 .. note:: 

1565 

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. 

1568 

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 "" 

1572 

1573 return f"{epoch}{self._major}.{self._minor}.{self._micro}" 

1574 

1575 def __str__(self) -> str: 

1576 """ 

1577 Return a string representation of this version number. 

1578 

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 "" 

1600 

1601 return result 

1602 

1603 

1604@export 

1605class PythonVersion(SemanticVersion): 

1606 """ 

1607 Represents a Python version. 

1608 """ 

1609 

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] = "!" 

1612 

1613 @classmethod 

1614 def FromSysVersionInfo(cls) -> PythonVersion: 

1615 """ 

1616 Create a Python version from :data:`sys.version_info`. 

1617 

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 

1622 

1623 if version_info.releaselevel == "final": 

1624 rl = ReleaseLevel.Final 

1625 number = None 

1626 else: # pragma: no cover 

1627 number = version_info.serial 

1628 

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}'.") 

1637 

1638 return cls(version_info.major, version_info.minor, version_info.micro, level=rl, number=number) 

1639 

1640 def __hash__(self) -> int: 

1641 """ 

1642 Compute a hash for this version number. 

1643 

1644 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the 

1645 version unhashable. 

1646 

1647 :returns: Hash of this version number. 

1648 """ 

1649 return super().__hash__() 

1650 

1651 def __str__(self) -> str: 

1652 """ 

1653 Return a string representation of this version number. 

1654 

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 "" 

1673 

1674 return result 

1675 

1676 

1677@export 

1678class CalendarVersion(Version): 

1679 """Representation of a calendar version number like ``2021.10``.""" 

1680 

1681 _PARTCOUNT: ClassVar[int] = 3 #: Number of numeric parts a version number of this class can carry. 

1682 

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. 

1691 

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. 

1704 

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) 

1724 

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. 

1729 

1730 Allowed prefix characters: 

1731 

1732 * ``v|V`` - version, public version, public release 

1733 * ``i|I`` - internal version, internal release 

1734 * ``r|R`` - release, revision 

1735 * ``rev|REV`` - revision 

1736 

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. 

1740 

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.") 

1762 

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 

1768 

1769 prefix = match["prefix"] 

1770 minor = match["minor"] 

1771 micro = match["micro"] 

1772 

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 

1777 

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)) 

1783 

1784 version = cls(*numbers, flags=Flags.Clean, prefix=prefix if prefix != "" else None) 

1785 

1786 if validator is not None and not validator(version): 

1787 raise VersionValidatorError(f"Failed to validate version string '{versionString}'.", version=version) 

1788 

1789 return version 

1790 

1791 @readonly 

1792 def Year(self) -> int: 

1793 """ 

1794 Read-only property to access the year part. 

1795 

1796 :returns: The year part. 

1797 """ 

1798 return self._major 

1799 

1800 def _equal(self, left: CalendarVersion, right: CalendarVersion) -> Nullable[bool]: 

1801 """ 

1802 Private helper method to compute the equality of two :class:`CalendarVersion` instances. 

1803 

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) 

1809 

1810 def _compare(self, left: CalendarVersion, right: CalendarVersion) -> Nullable[bool]: 

1811 """ 

1812 Private helper method to compute the comparison of two :class:`CalendarVersion` instances. 

1813 

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 

1824 

1825 if left._minor < right._minor: 

1826 return True 

1827 elif left._minor > right._minor: 

1828 return False 

1829 

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 

1834 

1835 return None 

1836 

1837 def __eq__(self, other: Any) -> bool: 

1838 """ 

1839 Compare two version numbers for equality. 

1840 

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). 

1844 

1845 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1846 number. 

1847 

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) 

1854 

1855 def __ne__(self, other: Any) -> bool: 

1856 """ 

1857 Compare two version numbers for inequality. 

1858 

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). 

1862 

1863 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1864 number. 

1865 

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) 

1872 

1873 def __lt__(self, other: Any) -> bool: 

1874 """ 

1875 Compare two version numbers if the version is less than the second operand. 

1876 

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). 

1880 

1881 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1882 number. 

1883 

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) 

1890 

1891 def __le__(self, other: Any) -> bool: 

1892 """ 

1893 Compare two version numbers if the version is less than or equal the second operand. 

1894 

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). 

1898 

1899 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1900 number. 

1901 

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) 

1908 

1909 def __gt__(self, other: Any) -> bool: 

1910 """ 

1911 Compare two version numbers if the version is greater than the second operand. 

1912 

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). 

1916 

1917 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1918 number. 

1919 

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) 

1926 

1927 def __ge__(self, other: Any) -> bool: 

1928 """ 

1929 Compare two version numbers if the version is greater than or equal the second operand. 

1930 

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). 

1934 

1935 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor 

1936 number. 

1937 

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) 

1944 

1945 def __hash__(self) -> int: 

1946 """ 

1947 Compute a hash for this version number. 

1948 

1949 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the 

1950 version unhashable. 

1951 

1952 :returns: Hash of this version number. 

1953 """ 

1954 return super().__hash__() 

1955 

1956 def __format__(self, formatSpec: str) -> str: 

1957 """ 

1958 Return a string representation of this version number according to the format specification. 

1959 

1960 .. topic:: Format Specifiers 

1961 

1962 * ``%M`` - major number (year) 

1963 * ``%m`` - minor number (month/week) 

1964 * ``%u`` - micro number (day) 

1965 

1966 :param formatSpec: The format specification. 

1967 :returns: Formatted version number. 

1968 """ 

1969 if formatSpec == "": 

1970 return self.__str__() 

1971 

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)) 

1978 

1979 return result.replace("%%", "%") 

1980 

1981 def __repr__(self) -> str: 

1982 """ 

1983 Return a normalized string representation of this version number. 

1984 

1985 .. note:: 

1986 

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. 

1989 

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 "" 

1994 

1995 return result 

1996 

1997 def __str__(self) -> str: 

1998 """ 

1999 Return a string representation of this version number with only the present parts. 

2000 

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 "" 

2007 

2008 return result 

2009 

2010 

2011@export 

2012class YearMonthVersion(CalendarVersion): 

2013 """Representation of a calendar version number made of year and month like ``2021.10``.""" 

2014 

2015 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and month. 

2016 

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. 

2028 

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) 

2047 

2048 @readonly 

2049 def Month(self) -> int: 

2050 """ 

2051 Read-only property to access the month part. 

2052 

2053 :returns: The month part. 

2054 """ 

2055 return self._minor 

2056 

2057 def __hash__(self) -> int: 

2058 """ 

2059 Compute a hash for this version number. 

2060 

2061 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the 

2062 version unhashable. 

2063 

2064 :returns: Hash of this version number. 

2065 """ 

2066 return super().__hash__() 

2067 

2068 

2069@export 

2070class YearWeekVersion(CalendarVersion): 

2071 """Representation of a calendar version number made of year and week like ``2021.47``.""" 

2072 

2073 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and week. 

2074 

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. 

2086 

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) 

2105 

2106 @readonly 

2107 def Week(self) -> int: 

2108 """ 

2109 Read-only property to access the week part. 

2110 

2111 :returns: The week part. 

2112 """ 

2113 return self._minor 

2114 

2115 def __hash__(self) -> int: 

2116 """ 

2117 Compute a hash for this version number. 

2118 

2119 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the 

2120 version unhashable. 

2121 

2122 :returns: Hash of this version number. 

2123 """ 

2124 return super().__hash__() 

2125 

2126 

2127@export 

2128class YearReleaseVersion(CalendarVersion): 

2129 """Representation of a calendar version number made of year and release per year like ``2021.2``.""" 

2130 

2131 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and release. 

2132 

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. 

2144 

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) 

2163 

2164 @readonly 

2165 def Release(self) -> int: 

2166 """ 

2167 Read-only property to access the release number. 

2168 

2169 :returns: The release number. 

2170 """ 

2171 return self._minor 

2172 

2173 def __hash__(self) -> int: 

2174 """ 

2175 Compute a hash for this version number. 

2176 

2177 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the 

2178 version unhashable. 

2179 

2180 :returns: Hash of this version number. 

2181 """ 

2182 return super().__hash__() 

2183 

2184 

2185@export 

2186class YearMonthDayVersion(CalendarVersion): 

2187 """Representation of a calendar version number made of year, month and day like ``2021.10.15``.""" 

2188 

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. 

2201 

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) 

2221 

2222 @readonly 

2223 def Month(self) -> int: 

2224 """ 

2225 Read-only property to access the month part. 

2226 

2227 :returns: The month part. 

2228 """ 

2229 return self._minor 

2230 

2231 @readonly 

2232 def Day(self) -> int: 

2233 """ 

2234 Read-only property to access the day part. 

2235 

2236 :returns: The day part. 

2237 """ 

2238 return self._micro 

2239 

2240 def __hash__(self) -> int: 

2241 """ 

2242 Compute a hash for this version number. 

2243 

2244 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the 

2245 version unhashable. 

2246 

2247 :returns: Hash of this version number. 

2248 """ 

2249 return super().__hash__() 

2250 

2251 

2252V = TypeVar("V", bound=Version) 

2253 

2254@export 

2255class RangeBoundHandling(Flag): 

2256 """ 

2257 A flag defining how to handle bounds in a range. 

2258 

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. 

2268 

2269 

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. 

2274 

2275 This version range works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes. 

2276 

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. 

2280 

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: 

2283 

2284 .. code-block:: python 

2285 

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. 

2293 

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. 

2302 

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. 

2306 

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 

2319 

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 

2324 

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 

2331 

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 

2336 

2337 self._lowerBound = lowerBound 

2338 self._upperBound = upperBound 

2339 self._boundHandling = boundHandling 

2340 

2341 @property 

2342 def LowerBound(self) -> Nullable[V]: 

2343 """ 

2344 Property to access the range's lower bound. 

2345 

2346 Assigning ``None`` leaves the bound unbound, opening the range in that direction. 

2347 

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 

2355 

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 

2362 

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 

2369 

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 

2374 

2375 self._lowerBound = value 

2376 

2377 @property 

2378 def UpperBound(self) -> Nullable[V]: 

2379 """ 

2380 Property to access the range's upper bound. 

2381 

2382 Assigning ``None`` leaves the bound unbound, opening the range in that direction. 

2383 

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 

2391 

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 

2398 

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 

2405 

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 

2410 

2411 self._upperBound = value 

2412 

2413 @property 

2414 def BoundHandling(self) -> RangeBoundHandling: 

2415 """ 

2416 Property to access the range's bound handling strategy. 

2417 

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 

2422 

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 

2429 

2430 self._boundHandling = value 

2431 

2432 @staticmethod 

2433 def _AreCompatible(left: Version, right: Version) -> bool: 

2434 """ 

2435 Check whether two versions' types can be related to each other. 

2436 

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`. 

2440 

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. 

2443 

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__ 

2450 

2451 return leftType is rightType or issubclass(leftType, rightType) or issubclass(rightType, leftType) 

2452 

2453 def _CheckCompatibility(self, other: Version) -> None: 

2454 """ 

2455 Check that a version can be related to this range's bounds. 

2456 

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. 

2461 

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 

2468 

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 

2474 

2475 def __and__(self, other: Any) -> VersionRange[V]: 

2476 """ 

2477 Compute the intersection of two version ranges. 

2478 

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. 

2482 

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 

2493 

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 

2501 

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 

2506 

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 

2524 

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 

2540 

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 

2546 

2547 boundHandling = RangeBoundHandling.BothBoundsInclusive 

2548 if lowerExclusive: 

2549 boundHandling |= RangeBoundHandling.LowerBoundExclusive 

2550 

2551 if upperExclusive: 

2552 boundHandling |= RangeBoundHandling.UpperBoundExclusive 

2553 

2554 return self.__class__(lBound, uBound, boundHandling) 

2555 

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). 

2559 

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 

2570 

2571 self._CheckCompatibility(other) 

2572 

2573 if self._upperBound is None: 

2574 return False 

2575 

2576 return self._upperBound < other 

2577 

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). 

2581 

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 

2592 

2593 self._CheckCompatibility(other) 

2594 

2595 if self._upperBound is None: 

2596 return False 

2597 

2598 if RangeBoundHandling.UpperBoundExclusive in self._boundHandling: 

2599 return self._upperBound < other 

2600 

2601 return self._upperBound <= other 

2602 

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). 

2606 

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 

2617 

2618 self._CheckCompatibility(other) 

2619 

2620 if self._lowerBound is None: 

2621 return False 

2622 

2623 return self._lowerBound > other 

2624 

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). 

2628 

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 

2639 

2640 self._CheckCompatibility(other) 

2641 

2642 if self._lowerBound is None: 

2643 return False 

2644 

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 

2647 

2648 return self._lowerBound >= other 

2649 

2650 def __contains__(self, version: Version) -> bool: 

2651 """ 

2652 Check if the version is in the version range. 

2653 

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 

2664 

2665 self._CheckCompatibility(version) 

2666 

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 

2674 

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 

2681 

2682 return True 

2683 

2684 

2685@export 

2686class VersionSet(Generic[V], metaclass=ExtendedType, slots=True): 

2687 """ 

2688 Representation of an ordered set of versions. 

2689 

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. 

2693 

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. 

2697 

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.") 

2706 

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 

2716 

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.") 

2719 

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__}") 

2724 

2725 self._items = list(sorted(versions)) 

2726 else: 

2727 raise TypeError("Parameter 'versions' is not an Iterable.") 

2728 

2729 def __and__(self, other: VersionSet[V]) -> VersionSet[V]: 

2730 """ 

2731 Compute intersection of two version sets. 

2732 

2733 :param other: Second set of versions. 

2734 :returns: Intersection of two version sets. 

2735 """ 

2736 selfIterator = self.__iter__() 

2737 otherIterator = other.__iter__() 

2738 

2739 result = [] 

2740 try: 

2741 selfValue = next(selfIterator) 

2742 otherValue = next(otherIterator) 

2743 

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) 

2753 

2754 except StopIteration: 

2755 pass 

2756 

2757 return VersionSet(result) 

2758 

2759 def __or__(self, other: VersionSet[V]) -> VersionSet[V]: 

2760 """ 

2761 Compute union of two version sets. 

2762 

2763 :param other: Second set of versions. 

2764 :returns: Union of two version sets. 

2765 """ 

2766 selfIterator = self.__iter__() 

2767 otherIterator = other.__iter__() 

2768 

2769 result = [] 

2770 try: 

2771 selfValue = next(selfIterator) 

2772 except StopIteration: 

2773 for otherValue in otherIterator: 

2774 result.append(otherValue) 

2775 

2776 try: 

2777 otherValue = next(otherIterator) 

2778 except StopIteration: 

2779 for selfValue in selfIterator: 

2780 result.append(selfValue) 

2781 

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) 

2791 

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) 

2801 

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) 

2810 

2811 break 

2812 

2813 try: 

2814 otherValue = next(otherIterator) 

2815 except StopIteration: 

2816 for selfValue in selfIterator: 

2817 result.append(selfValue) 

2818 

2819 break 

2820 

2821 return VersionSet(result) 

2822 

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). 

2826 

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 

2837 

2838 return self._items[-1] < other 

2839 

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). 

2843 

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 

2854 

2855 return self._items[-1] <= other 

2856 

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). 

2860 

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 

2871 

2872 return self._items[0] > other 

2873 

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). 

2877 

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 

2888 

2889 return self._items[0] >= other 

2890 

2891 def __contains__(self, version: V) -> bool: 

2892 """ 

2893 Checks if the version a member of the set. 

2894 

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 

2899 

2900 def __len__(self) -> int: 

2901 """ 

2902 Returns the number of members in the set. 

2903 

2904 :returns: Number of set members. 

2905 """ 

2906 return len(self._items) 

2907 

2908 def __iter__(self) -> Iterator[V]: 

2909 """ 

2910 Returns an iterator to iterate all versions of this set from lowest to highest. 

2911 

2912 :returns: Iterator to iterate versions. 

2913 """ 

2914 return self._items.__iter__() 

2915 

2916 def __getitem__(self, index: int) -> V: 

2917 """ 

2918 Access to a version of a set by index. 

2919 

2920 :param index: The index of the version to access. 

2921 :returns: The indexed version. 

2922 

2923 .. hint:: 

2924 

2925 Versions are ordered from lowest to highest version number. 

2926 """ 

2927 return self._items[index] 

2928 

2929 

2930#: The :class:`Version` class under a name no constraint shadows with a property of its own. 

2931_VersionType = Version 

2932 

2933 

2934@export 

2935class VersionComparison(Enum): 

2936 """ 

2937 The comparison one constraint of a :class:`VersionExpression` applies to a version. 

2938 

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 """ 

2944 

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). 

2954 

2955 def __str__(self) -> str: 

2956 """ 

2957 Return the operator in its canonical spelling. 

2958 

2959 :returns: The comparison's operator. 

2960 """ 

2961 return self.value 

2962 

2963 

2964@export 

2965class VersionConstraint(Generic[V], metaclass=ExtendedType, slots=True): 

2966 """ 

2967 One comparison of a :class:`VersionExpression`, such as ``>=1.2.0``. 

2968 

2969 A constraint is a container of the versions satisfying it, so membership is asked with ``in``: 

2970 

2971 .. code-block:: python 

2972 

2973 constraint = VersionConstraint(VersionComparison.GreaterThanOrEqual, SemanticVersion.Parse("1.2.0")) # >=1.2.0 

2974 SemanticVersion.Parse("1.5.0") in constraint # True 

2975 

2976 .. seealso:: 

2977 

2978 :class:`CompatibleVersionConstraint` 

2979 |rarr| The constraint implementing :attr:`~VersionComparison.CompatibleRelease`. 

2980 """ 

2981 

2982 _comparison: VersionComparison #: The comparison this constraint applies. 

2983 _version: V #: The version the compared version is held against. 

2984 

2985 def __init__(self, comparison: VersionComparison, version: V) -> None: 

2986 """ 

2987 Initialize a constraint from a comparison and the version it compares against. 

2988 

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 

3002 

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 

3007 

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 

3012 

3013 self._comparison = comparison 

3014 self._version = version 

3015 

3016 @readonly 

3017 def Comparison(self) -> VersionComparison: 

3018 """ 

3019 Read-only property to access the comparison this constraint applies (:attr:`_comparison`). 

3020 

3021 :returns: The comparison. 

3022 """ 

3023 return self._comparison 

3024 

3025 @readonly 

3026 def Version(self) -> V: 

3027 """ 

3028 Read-only property to access the version this constraint compares against (:attr:`_version`). 

3029 

3030 :returns: The version. 

3031 """ 

3032 return self._version 

3033 

3034 def __contains__(self, version: V) -> bool: 

3035 """ 

3036 Check if a version satisfies this constraint. 

3037 

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 

3048 

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 

3061 

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 

3065 

3066 def ToVersionRange(self) -> VersionRange[_VersionType]: 

3067 """ 

3068 Convert this constraint into the :class:`VersionRange` it describes. 

3069 

3070 Five of the six comparisons are intervals once a bound may be unbound: 

3071 

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. 

3077 

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. 

3081 

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) 

3097 

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 

3101 

3102 def __str__(self) -> str: 

3103 """ 

3104 Return the constraint in its canonical spelling. 

3105 

3106 :returns: The operator followed by the version. 

3107 """ 

3108 return f"{self._comparison}{self._version}" 

3109 

3110 

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) 

3117 

3118 

3119@export 

3120class RangeVersionConstraint(VersionConstraint[V]): 

3121 """ 

3122 Base-class of the shorthand constraints meaning *at least this version, and below a derived bound*. 

3123 

3124 Every packaging ecosystem has one of these, spells it differently, and derives its upper bound by a **different** 

3125 rule: 

3126 

3127 * :pep:`440` writes ``~=``, 

3128 * npm writes ``^`` and ``~``, 

3129 * RubyGems writes ``~>``. 

3130 

3131 The rule is the only thing that differs, so it is what a derived class supplies in :meth:`_DeriveUpperBound`. 

3132 

3133 .. seealso:: 

3134 

3135 :class:`CompatibleVersionConstraint` 

3136 |rarr| :pep:`440`'s ``~=``. 

3137 :class:`CaretVersionConstraint` 

3138 |rarr| npm's ``^``. 

3139 :class:`TildeVersionConstraint` 

3140 |rarr| npm's ``~``. 

3141 """ 

3142 

3143 _upperBound: SemanticVersion #: The first version outside the constraint, derived from the written version. 

3144 

3145 def __init__(self, comparison: VersionComparison, version: V) -> None: 

3146 """ 

3147 Initialize a shorthand constraint and derive its upper bound. 

3148 

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 

3159 

3160 super().__init__(comparison, version) 

3161 

3162 self._upperBound = self._DeriveUpperBound(version) 

3163 

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. 

3168 

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. 

3171 

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 """ 

3176 

3177 @readonly 

3178 def UpperBound(self) -> SemanticVersion: 

3179 """ 

3180 Read-only property to access the first version outside this constraint (:attr:`_upperBound`). 

3181 

3182 :returns: The exclusive upper bound derived from the written version. 

3183 """ 

3184 return self._upperBound 

3185 

3186 def __contains__(self, version: V) -> bool: 

3187 """ 

3188 Check if a version is within this constraint. 

3189 

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 

3198 

3199 return self._version <= version < self._upperBound 

3200 

3201 def ToVersionRange(self) -> VersionRange[_VersionType]: 

3202 """ 

3203 Convert this constraint into the :class:`VersionRange` it describes. 

3204 

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. 

3207 

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 ) 

3215 

3216 

3217@export 

3218class CompatibleVersionConstraint(RangeVersionConstraint[V]): 

3219 """ 

3220 The *compatible release* constraint, written ``~=`` by :pep:`440`. 

3221 

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. 

3225 

3226 .. code-block:: python 

3227 

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 """ 

3232 

3233 def __init__(self, version: V) -> None: 

3234 """ 

3235 Initialize a compatible-release constraint from the version it is written with. 

3236 

3237 :param version: The version to be compatible with, with at least two parts. 

3238 """ 

3239 super().__init__(VersionComparison.CompatibleRelease, version) 

3240 

3241 def _DeriveUpperBound(self, version: SemanticVersion) -> SemanticVersion: 

3242 """ 

3243 Drop the last part written and increment the one that becomes the last. 

3244 

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) 

3257 

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 

3261 

3262 

3263@export 

3264class CaretVersionConstraint(RangeVersionConstraint[V]): 

3265 """ 

3266 npm's ``^``: the version must not change the **leftmost non-zero** part of the one written. 

3267 

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``. 

3271 

3272 .. note:: 

3273 

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 """ 

3277 

3278 def __init__(self, version: V) -> None: 

3279 """ 

3280 Initialize a caret constraint from the version it is written with. 

3281 

3282 :param version: The version to be compatible with. 

3283 """ 

3284 super().__init__(VersionComparison.Caret, version) 

3285 

3286 def _DeriveUpperBound(self, version: SemanticVersion) -> SemanticVersion: 

3287 """ 

3288 Increment the leftmost non-zero part that was actually written. 

3289 

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) 

3299 

3300 return versionType(0, 0, version.Patch + 1, epoch=epoch) 

3301 

3302 

3303@export 

3304class TildeVersionConstraint(RangeVersionConstraint[V]): 

3305 """ 

3306 npm's ``~``: the version must not change the minor part of the one written. 

3307 

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``. 

3310 

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 """ 

3314 

3315 def __init__(self, version: V) -> None: 

3316 """ 

3317 Initialize a tilde constraint from the version it is written with. 

3318 

3319 :param version: The version to be compatible with. 

3320 """ 

3321 super().__init__(VersionComparison.Tilde, version) 

3322 

3323 def _DeriveUpperBound(self, version: SemanticVersion) -> SemanticVersion: 

3324 """ 

3325 Increment the minor part, or the major one when no minor part was written. 

3326 

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) 

3334 

3335 return versionType(version.Major + 1, epoch=epoch) 

3336 

3337 

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. 

3346 

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. 

3350 

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``. 

3354 

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. 

3359 

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__`. 

3362 

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) 

3370 

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))) 

3378 

3379 return re_compile(rf"({alternation})?\s*([^\s{excludedCharacters}]+)") 

3380 

3381 

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``. 

3386 

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. 

3390 

3391 .. code-block:: python 

3392 

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 

3396 

3397 SemanticVersion.Parse("4.2.0") in VersionExpression.Parse("") # True - no constraints matches anything 

3398 

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. 

3403 

3404 .. seealso:: 

3405 

3406 :class:`PythonVersionExpression` 

3407 |rarr| The dialect :pep:`440` defines, which a Python requirement file writes. 

3408 """ 

3409 

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 } 

3419 

3420 #: Characters separating one constraint from the next. Whitespace always separates as well. 

3421 _SEPARATORS: ClassVar[str] = "," 

3422 

3423 #: The :class:`Version` class this dialect parses its versions as, unless a caller names another. 

3424 _VERSION_TYPE: ClassVar[type[Version]] = SemanticVersion 

3425 

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]] = {} 

3429 

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) 

3433 

3434 _constraints: tuple[VersionConstraint[V], ...] #: The constraints a version has to satisfy, all of them. 

3435 

3436 def __init__(self, constraints: Iterable[VersionConstraint[V]] = ()) -> None: 

3437 """ 

3438 Initialize an expression from its constraints. 

3439 

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 

3451 

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 

3458 

3459 self._constraints = items 

3460 

3461 def __init_subclass__(cls, **kwargs: Any) -> None: 

3462 """ 

3463 Compile the constraint pattern of a newly defined dialect. 

3464 

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`. 

3467 

3468 :param kwargs: Keyword arguments passed on to the base implementation. 

3469 """ 

3470 super().__init_subclass__(**kwargs) 

3471 

3472 cls._CONSTRAINT_PATTERN = _BuildConstraintPattern(cls._OPERATORS, cls._SEPARATORS, cls._VERSION_TYPE) 

3473 

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. 

3478 

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. 

3482 

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. 

3485 

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() 

3505 

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 

3511 

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 

3517 

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 

3527 

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() 

3533 

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 

3538 

3539 return cls(constraints) 

3540 

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`). 

3545 

3546 :returns: The constraints, empty if the expression matches every version. 

3547 """ 

3548 return self._constraints 

3549 

3550 @readonly 

3551 def MatchesAnyVersion(self) -> bool: 

3552 """ 

3553 Read-only property to return whether this expression constrains nothing. 

3554 

3555 :returns: ``True``, if the expression has no constraints and every version satisfies it. 

3556 """ 

3557 return len(self._constraints) == 0 

3558 

3559 def __contains__(self, version: V) -> bool: 

3560 """ 

3561 Check if a version satisfies every constraint of this expression. 

3562 

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 

3572 

3573 return all(version in constraint for constraint in self._constraints) 

3574 

3575 def ToVersionRange(self) -> VersionRange[Version]: 

3576 """ 

3577 Convert this expression into the single :class:`VersionRange` it describes. 

3578 

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. 

3582 

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. 

3587 

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() 

3596 

3597 return versionRange 

3598 

3599 def __len__(self) -> int: 

3600 """ 

3601 Return the number of constraints in this expression. 

3602 

3603 :returns: Number of constraints. 

3604 """ 

3605 return len(self._constraints) 

3606 

3607 def __iter__(self) -> Iterator[VersionConstraint[V]]: 

3608 """ 

3609 Iterate the constraints of this expression, in the order they were written. 

3610 

3611 :returns: An iterator over the constraints. 

3612 """ 

3613 return iter(self._constraints) 

3614 

3615 def __str__(self) -> str: 

3616 """ 

3617 Return the expression in this dialect's spelling. 

3618 

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. 

3623 

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] = [] 

3628 

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)) 

3636 

3637 return separator.join(spelled) 

3638 

3639 

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. 

3644 

3645 It adds the compatible release operator ``~=`` to the six ordering comparisons and parses its versions as 

3646 :class:`PythonVersion`: 

3647 

3648 .. code-block:: python 

3649 

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 

3653 

3654 .. seealso:: 

3655 

3656 :class:`CompatibleVersionConstraint` 

3657 |rarr| What ``~=`` is parsed into, and how its upper bound is derived. 

3658 """ 

3659 

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 } 

3665 

3666 #: :pep:`440` versions, so an epoch, a release candidate or a post-release parses. 

3667 _VERSION_TYPE: ClassVar[type[Version]] = PythonVersion 

3668 

3669 #: ``~=`` derives an upper bound, so it is not a plain comparison. 

3670 _SHORTHANDS: ClassVar[dict[VersionComparison, type]] = { 

3671 VersionComparison.CompatibleRelease: CompatibleVersionConstraint, 

3672 } 

3673 

3674 

3675@export 

3676class NPMVersionExpression(VersionExpression[V]): 

3677 """ 

3678 A version expression in npm's dialect, which is what a ``package.json`` dependency writes. 

3679 

3680 npm differs from :pep:`440` in three ways that matter to a parser: 

3681 

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. 

3685 

3686 It adds ``^`` and ``~``, which are not :pep:`440`'s ``~=``: 

3687 

3688 .. code-block:: python 

3689 

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 

3693 

3694 .. note:: 

3695 

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. 

3699 

3700 .. seealso:: 

3701 

3702 :class:`CaretVersionConstraint` |br| 

3703 :class:`TildeVersionConstraint` 

3704 """ 

3705 

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 } 

3716 

3717 #: npm separates constraints by whitespace alone; a comma is a syntax error there. 

3718 _SEPARATORS: ClassVar[str] = "" 

3719 

3720 #: npm is strict semantic versioning. 

3721 _VERSION_TYPE: ClassVar[type[Version]] = SemanticVersion 

3722 

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 } 

3728 

3729 

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. 

3734 

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. 

3738 

3739 .. code-block:: python 

3740 

3741 expression = DebianVersionExpression.Parse(">> 1.2.3") 

3742 SemanticVersion.Parse("1.3.0") in expression # True 

3743 

3744 .. note:: 

3745 

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. 

3749 

3750 .. attention:: 

3751 

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 """ 

3756 

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 } 

3765 

3766 #: Debian's comma separates dependencies rather than constraints, but accepting it costs nothing. 

3767 _SEPARATORS: ClassVar[str] = "," 

3768 

3769 #: The closest pyTooling has to a Debian version - see the class doc-string's caveat. 

3770 _VERSION_TYPE: ClassVar[type[Version]] = SemanticVersion