Coverage for pyTooling/Versioning/__init__.py: 90%

1463 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-25 13:06 +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 sys import version_info 

51from typing import Optional as Nullable, Union, Any, ClassVar, Generic, TypeVar, Iterable, Iterator, \ 

52 Self 

53 

54from pyTooling.Decorators import export, readonly 

55from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride 

56from pyTooling.Exceptions import ToolingException 

57from pyTooling.Common import getFullyQualifiedName, getTypeName 

58from pyTooling.DataModel import Predicate 

59 

60 

61@export 

62class VersionValidatorError(ToolingException): 

63 """ 

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

65 

66 The version string itself was well-formed - it parsed - so this is not a :exc:`ValueError` about the input, but 

67 a statement that the resulting version is not acceptable to the caller. The version that failed is carried in 

68 :attr:`Version`, so a caller can report what was wrong with it. 

69 """ 

70 

71 _version: Version #: The version rejected by a validator. 

72 

73 def __init__(self, message: str, /, *, version: Version) -> None: 

74 """ 

75 Initializes the exception with the rejected version. 

76 

77 :param message: The exception's message. 

78 :param version: The version the validator rejected. 

79 """ 

80 super().__init__(message) 

81 self._version = version 

82 

83 @readonly 

84 def Version(self) -> Version: 

85 """ 

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

87 

88 :returns: The rejected version. 

89 """ 

90 return self._version 

91 

92 

93@export 

94class Parts(Flag): 

95 """Enumeration describing parts of a version number that can be present.""" 

96 Unknown = 0 #: Undocumented 

97 Epoch = 1 #: Epoch is present. (e.g. E in ``E:1.2.3`` or ``vE!1.2.3``) 

98 Major = 2 #: Major number is present. (e.g. X in ``vX.0.0``). 

99 Year = 2 #: Year is present. (e.g. X in ``XXXX.10``). 

100 Minor = 4 #: Minor number is present. (e.g. Y in ``v0.Y.0``). 

101 Month = 4 #: Month is present. (e.g. X in ``2024.YY``). 

102 Week = 4 #: Week is present. (e.g. X in ``2024.YY``). 

103 Micro = 8 #: Patch number is present. (e.g. Z in ``v0.0.Z``). 

104 Patch = 8 #: Patch number is present. (e.g. Z in ``v0.0.Z``). 

105 Day = 8 #: Day is present. (e.g. X in ``2024.10.ZZ``). 

106 Level = 16 #: Release level is present. 

107 Dev = 32 #: Development part is present. 

108 Build = 64 #: Build number is present. (e.g. bbbb in ``v0.0.0.bbbb``) 

109 Post = 128 #: Post-release number is present. 

110 Prefix = 256 #: Prefix is present. 

111 Postfix = 512 #: Postfix is present. 

112 Hash = 1024 #: Hash is present. 

113# AHead = 256 

114 

115 

116@export 

117class ReleaseLevel(Enum): 

118 """Enumeration describing the version's maturity level.""" 

119 Final = 0 #: 

120 ReleaseCandidate = -10 #: 

121 Development = -20 #: 

122 Gamma = -30 #: 

123 Beta = -40 #: 

124 Alpha = -50 #: 

125 

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

127 """ 

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

129 

130 :param other: Operand to compare against. 

131 :returns: ``True``, if release level is equal the second operand's release level. 

132 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string. 

133 """ 

134 if isinstance(other, str): 134 ↛ 135line 134 didn't jump to line 135 because the condition on line 134 was never true

135 other = ReleaseLevel(other) 

136 

137 if not isinstance(other, ReleaseLevel): 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true

138 ex = TypeError("Second operand is not supported by == operator.") 

139 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

140 ex.add_note(f"Supported types for second operand: {getTypeName(self)} or 'str'.") 

141 raise ex 

142 

143 return self is other 

144 

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

146 """ 

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

148 

149 :param other: Operand to compare against. 

150 :returns: ``True``, if release level is unequal the second operand's release level. 

151 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string. 

152 """ 

153 if isinstance(other, str): 

154 other = ReleaseLevel(other) 

155 

156 if not isinstance(other, ReleaseLevel): 

157 ex = TypeError("Second operand is not supported by != operator.") 

158 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

159 ex.add_note(f"Supported types for second operand: {getTypeName(self)} or 'str'.") 

160 raise ex 

161 

162 return self is not other 

163 

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

165 """ 

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

167 

168 :param other: Operand to compare against. 

169 :returns: ``True``, if release level is less than the second operand. 

170 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string. 

171 """ 

172 if isinstance(other, str): 172 ↛ 173line 172 didn't jump to line 173 because the condition on line 172 was never true

173 other = ReleaseLevel(other) 

174 

175 if not isinstance(other, ReleaseLevel): 175 ↛ 176line 175 didn't jump to line 176 because the condition on line 175 was never true

176 ex = TypeError("Second operand is not supported by < operator.") 

177 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

178 ex.add_note(f"Supported types for second operand: {getTypeName(self)} or 'str'.") 

179 raise ex 

180 

181 return self.value < other.value 

182 

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

184 """ 

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

186 

187 :param other: Operand to compare against. 

188 :returns: ``True``, if release level is less than or equal the second operand. 

189 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string. 

190 """ 

191 if isinstance(other, str): 

192 other = ReleaseLevel(other) 

193 

194 if not isinstance(other, ReleaseLevel): 

195 ex = TypeError("Second operand is not supported by <=>= operator.") 

196 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

197 ex.add_note(f"Supported types for second operand: {getTypeName(self)} or 'str'.") 

198 raise ex 

199 

200 return self.value <= other.value 

201 

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

203 """ 

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

205 

206 :param other: Operand to compare against. 

207 :returns: ``True``, if release level is greater than the second operand. 

208 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string. 

209 """ 

210 if isinstance(other, str): 210 ↛ 211line 210 didn't jump to line 211 because the condition on line 210 was never true

211 other = ReleaseLevel(other) 

212 

213 if not isinstance(other, ReleaseLevel): 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true

214 ex = TypeError("Second operand is not supported by > operator.") 

215 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

216 ex.add_note(f"Supported types for second operand: {getTypeName(self)} or 'str'.") 

217 raise ex 

218 

219 return self.value > other.value 

220 

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

222 """ 

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

224 

225 :param other: Operand to compare against. 

226 :returns: ``True``, if release level is greater than or equal the second operand. 

227 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string. 

228 """ 

229 if isinstance(other, str): 

230 other = ReleaseLevel(other) 

231 

232 if not isinstance(other, ReleaseLevel): 

233 ex = TypeError("Second operand is not supported by >= operator.") 

234 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

235 ex.add_note(f"Supported types for second operand: {getTypeName(self)} or 'str'.") 

236 raise ex 

237 

238 return self.value >= other.value 

239 

240 def __hash__(self) -> int: 

241 """ 

242 Compute a hash for this release level, so it can be used as a key in a dictionary or an element of a set. 

243 

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

245 

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

247 """ 

248 return hash(self.value) 

249 

250 def __str__(self) -> str: 

251 """ 

252 Returns the release level's string equivalent. 

253 

254 :returns: The string equivalent of the release level. 

255 :raises ToolingException: If the release level is unknown, so it has no string equivalent. 

256 """ 

257 if self is ReleaseLevel.Final: 

258 return "final" 

259 elif self is ReleaseLevel.ReleaseCandidate: 

260 return "rc" 

261 elif self is ReleaseLevel.Development: 261 ↛ 262line 261 didn't jump to line 262 because the condition on line 261 was never true

262 return "dev" 

263 elif self is ReleaseLevel.Beta: 263 ↛ 264line 263 didn't jump to line 264 because the condition on line 263 was never true

264 return "beta" 

265 elif self is ReleaseLevel.Alpha: 265 ↛ 268line 265 didn't jump to line 268 because the condition on line 265 was always true

266 return "alpha" 

267 

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

269 

270 

271@export 

272class Flags(Flag): 

273 """State enumeration, if a (tagged) version is build from a clean or dirty working directory.""" 

274 NoVCS = 0 #: No Version Control System VCS 

275 Clean = 1 #: A versioned build was created from a *clean* working directory. 

276 Dirty = 2 #: A versioned build was created from a *dirty* working directory. 

277 

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

279 SVN = 32 #: Subversion (SVN) 

280 Git = 64 #: Git 

281 Hg = 128 #: Mercurial (Hg) 

282 

283 

284@export 

285def WordSizeValidator( 

286 bits: Nullable[int] = None, 

287 majorBits: Nullable[int] = None, 

288 minorBits: Nullable[int] = None, 

289 microBits: Nullable[int] = None, 

290 buildBits: Nullable[int] = None 

291): 

292 """ 

293 A factory function to return a validator for Version instances for a positive integer range based on word-sizes in bits. 

294 

295 :param bits: Optional, number of bits to encode any positive version number part. 

296 :param majorBits: Optional, number of bits to encode a positive major number in a version. 

297 :param minorBits: Optional, number of bits to encode a positive minor number in a version. 

298 :param microBits: Optional, number of bits to encode a positive micro number in a version. 

299 :param buildBits: Optional, number of bits to encode a positive build number in a version. 

300 :returns: A validation function for Version instances. 

301 """ 

302 majorMax = minorMax = microMax = buildMax = -1 

303 if bits is not None: 

304 majorMax = minorMax = microMax = buildMax = 2**bits - 1 

305 

306 if majorBits is not None: 

307 majorMax = 2**majorBits - 1 

308 

309 if minorBits is not None: 

310 minorMax = 2**minorBits - 1 

311 

312 if microBits is not None: 

313 microMax = 2 ** microBits - 1 

314 

315 if buildBits is not None: 315 ↛ 316line 315 didn't jump to line 316 because the condition on line 315 was never true

316 buildMax = 2**buildBits - 1 

317 

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

319 """ 

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

321 

322 :param version: The version to validate. 

323 :returns: ``True``, if every part fits into its word size. 

324 :raises ValueError: If a part exceeds the maximum value of its word size. 

325 """ 

326 if Parts.Major in version._parts and version._major > majorMax: 

327 raise ValueError(f"Field 'Version.Major' > {majorMax}.") 

328 

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

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

331 

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

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

334 

335 if Parts.Build in version._parts and version._build > buildMax: 335 ↛ 336line 335 didn't jump to line 336 because the condition on line 335 was never true

336 raise ValueError(f"Field 'Version.Build' > {buildMax}.") 

337 

338 return True 

339 

340 return validator 

341 

342 

343@export 

344def MaxValueValidator( 

345 max: Nullable[int] = None, 

346 majorMax: Nullable[int] = None, 

347 minorMax: Nullable[int] = None, 

348 microMax: Nullable[int] = None, 

349 buildMax: Nullable[int] = None 

350): 

351 """ 

352 A factory function to return a validator for Version instances checking for a positive integer range [0..max]. 

353 

354 :param max: Optional, the upper bound for any positive version number part. 

355 :param majorMax: Optional, the upper bound for the positive major number. 

356 :param minorMax: Optional, the upper bound for the positive minor number. 

357 :param microMax: Optional, the upper bound for the positive micro number. 

358 :param buildMax: Optional, the upper bound for the positive build number. 

359 :returns: A validation function for Version instances. 

360 """ 

361 if max is not None: 361 ↛ 364line 361 didn't jump to line 364 because the condition on line 361 was always true

362 majorMax = minorMax = microMax = buildMax = max 

363 

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

365 """ 

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

367 

368 :param version: The version to validate. 

369 :returns: ``True``, if every part is within its maximum. 

370 :raises ValueError: If a part exceeds its maximum value. 

371 """ 

372 if Parts.Major in version._parts and version._major > majorMax: 

373 raise ValueError(f"Field 'Version.Major' > {majorMax}.") 

374 

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

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

377 

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

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

380 

381 if Parts.Build in version._parts and version._build > buildMax: 381 ↛ 382line 381 didn't jump to line 382 because the condition on line 381 was never true

382 raise ValueError(f"Field 'Version.Build' > {buildMax}.") 

383 

384 return True 

385 

386 return validator 

387 

388 

389@export 

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

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

392 

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

394 

395 #: Separator between an epoch and the rest of the version number. Debian writes ``2:1.2.3``; PEP 440 writes 

396 #: ``2!1.2.3``, so :class:`PythonVersion` overrides this. 

397 _EPOCH_SEPARATOR: ClassVar[str] = ":" 

398 

399 _parts: Parts #: Integer flag enumeration of present parts in a version number. 

400 _prefix: str #: Prefix string 

401 _epoch: int #: Epoch, which outranks every other part of the version number. 

402 _major: int #: Major number part of the version number. 

403 _minor: int #: Minor number part of the version number. 

404 _micro: int #: Micro number part of the version number. 

405 _releaseLevel: ReleaseLevel #: Release level (alpha, beta, rc, final, ...). 

406 _releaseNumber: int #: Release number (Python calls this a serial). 

407 _post: int #: Post-release version number part. 

408 _dev: int #: Development number 

409 _build: int #: Build number part of the version number. 

410 _postfix: str #: Postfix string 

411 _hash: str #: Hash from version control system. 

412 _flags: Flags #: State if the version in a working directory is clean or dirty compared to a tagged version. 

413 

414 def __init__( 

415 self, 

416 major: int, 

417 minor: Nullable[int] = None, 

418 micro: Nullable[int] = None, 

419 level: Nullable[ReleaseLevel] = ReleaseLevel.Final, 

420 number: Nullable[int] = None, 

421 post: Nullable[int] = None, 

422 dev: Nullable[int] = None, 

423 *, 

424 epoch: Nullable[int] = None, 

425 build: Nullable[int] = None, 

426 postfix: Nullable[str] = None, 

427 prefix: Nullable[str] = None, 

428 hash: Nullable[str] = None, 

429 flags: Flags = Flags.NoVCS 

430 ) -> None: 

431 """ 

432 Initializes a version number representation. 

433 

434 :param major: Major number part of the version number. 

435 :param minor: Optional, minor number part of the version number. 

436 :param micro: Optional, micro (patch) number part of the version number. 

437 :param level: Optional, release level (alpha, beta, release candidate, final, ...) of the version number. 

438 :param number: Optional, release number part (in combination with release level) of the version number. 

439 :param post: Optional, post number part of the version number. 

440 :param dev: Optional, development number part of the version number. 

441 :param epoch: Optional, the version number's epoch, which outranks every other part. 

442 :param build: Optional, build number part of the version number. 

443 :param postfix: Optional, the version number's postfix. 

444 :param prefix: Optional, the version number's prefix. 

445 :param hash: Optional, postfix string. 

446 :param flags: Optional, the version number's flags. 

447 :raises TypeError: If parameter 'major' is not of type integer. 

448 :raises ValueError: If parameter 'major' is a negative number. 

449 :raises TypeError: If parameter 'minor' is not of type integer. 

450 :raises ValueError: If parameter 'minor' is a negative number. 

451 :raises TypeError: If parameter 'micro' is not of type integer. 

452 :raises ValueError: If parameter 'micro' is a negative number. 

453 :raises TypeError: If parameter 'epoch' is not of type integer. 

454 :raises ValueError: If parameter 'epoch' is a negative number. 

455 :raises TypeError: If parameter 'build' is not of type integer. 

456 :raises ValueError: If parameter 'build' is a negative number. 

457 :raises TypeError: If parameter 'prefix' is not of type string. 

458 :raises TypeError: If parameter 'postfix' is not of type string. 

459 """ 

460 self.__hash = None 

461 

462 if not isinstance(major, int): 

463 ex = TypeError("Parameter 'major' is not of type 'int'.") 

464 ex.add_note(f"Got type '{getFullyQualifiedName(major)}'.") 

465 raise ex 

466 elif major < 0: 

467 raise ValueError("Parameter 'major' is negative.") 

468 

469 self._parts = Parts.Major 

470 self._major = major 

471 

472 if epoch is not None: 

473 if not isinstance(epoch, int): 

474 ex = TypeError("Parameter 'epoch' is not of type 'int'.") 

475 ex.add_note(f"Got type '{getFullyQualifiedName(epoch)}'.") 

476 raise ex 

477 elif epoch < 0: 

478 raise ValueError("Parameter 'epoch' is negative.") 

479 

480 self._parts |= Parts.Epoch 

481 self._epoch = epoch 

482 else: 

483 self._epoch = 0 

484 

485 if minor is not None: 

486 if not isinstance(minor, int): 

487 ex = TypeError("Parameter 'minor' is not of type 'int'.") 

488 ex.add_note(f"Got type '{getFullyQualifiedName(minor)}'.") 

489 raise ex 

490 elif minor < 0: 

491 raise ValueError("Parameter 'minor' is negative.") 

492 

493 self._parts |= Parts.Minor 

494 self._minor = minor 

495 else: 

496 self._minor = 0 

497 

498 if micro is not None: 

499 if not isinstance(micro, int): 

500 ex = TypeError("Parameter 'micro' is not of type 'int'.") 

501 ex.add_note(f"Got type '{getFullyQualifiedName(micro)}'.") 

502 raise ex 

503 elif micro < 0: 

504 raise ValueError("Parameter 'micro' is negative.") 

505 

506 self._parts |= Parts.Micro 

507 self._micro = micro 

508 else: 

509 self._micro = 0 

510 

511 if level is None: 

512 raise ValueError("Parameter 'level' is None.") 

513 elif not isinstance(level, ReleaseLevel): 

514 ex = TypeError("Parameter 'level' is not of type 'ReleaseLevel'.") 

515 ex.add_note(f"Got type '{getFullyQualifiedName(level)}'.") 

516 raise ex 

517 elif level is ReleaseLevel.Final: 

518 if number is not None: 

519 raise ValueError("Parameter 'number' must be None, if parameter 'level' is 'Final'.") 

520 

521 self._parts |= Parts.Level 

522 self._releaseLevel = level 

523 self._releaseNumber = 0 

524 else: 

525 self._parts |= Parts.Level 

526 self._releaseLevel = level 

527 

528 if number is not None: 

529 if not isinstance(number, int): 

530 ex = TypeError("Parameter 'number' is not of type 'int'.") 

531 ex.add_note(f"Got type '{getFullyQualifiedName(number)}'.") 

532 raise ex 

533 elif number < 0: 

534 raise ValueError("Parameter 'number' is negative.") 

535 

536 self._releaseNumber = number 

537 else: 

538 self._releaseNumber = 0 

539 

540 if dev is not None: 

541 if not isinstance(dev, int): 

542 ex = TypeError("Parameter 'dev' is not of type 'int'.") 

543 ex.add_note(f"Got type '{getFullyQualifiedName(dev)}'.") 

544 raise ex 

545 elif dev < 0: 

546 raise ValueError("Parameter 'dev' is negative.") 

547 

548 self._parts |= Parts.Dev 

549 self._dev = dev 

550 else: 

551 self._dev = 0 

552 

553 if post is not None: 

554 if not isinstance(post, int): 

555 ex = TypeError("Parameter 'post' is not of type 'int'.") 

556 ex.add_note(f"Got type '{getFullyQualifiedName(post)}'.") 

557 raise ex 

558 elif post < 0: 

559 raise ValueError("Parameter 'post' is negative.") 

560 

561 self._parts |= Parts.Post 

562 self._post = post 

563 else: 

564 self._post = 0 

565 

566 if build is not None: 

567 if not isinstance(build, int): 

568 ex = TypeError("Parameter 'build' is not of type 'int'.") 

569 ex.add_note(f"Got type '{getFullyQualifiedName(build)}'.") 

570 raise ex 

571 elif build < 0: 

572 raise ValueError("Parameter 'build' is negative.") 

573 

574 self._build = build 

575 self._parts |= Parts.Build 

576 else: 

577 self._build = 0 

578 

579 if postfix is not None: 

580 if not isinstance(postfix, str): 

581 ex = TypeError("Parameter 'postfix' is not of type 'str'.") 

582 ex.add_note(f"Got type '{getFullyQualifiedName(postfix)}'.") 

583 raise ex 

584 

585 self._parts |= Parts.Postfix 

586 self._postfix = postfix 

587 else: 

588 self._postfix = "" 

589 

590 if prefix is not None: 

591 if not isinstance(prefix, str): 

592 ex = TypeError("Parameter 'prefix' is not of type 'str'.") 

593 ex.add_note(f"Got type '{getFullyQualifiedName(prefix)}'.") 

594 raise ex 

595 

596 self._parts |= Parts.Prefix 

597 self._prefix = prefix 

598 else: 

599 self._prefix = "" 

600 

601 if hash is not None: 

602 if not isinstance(hash, str): 

603 ex = TypeError("Parameter 'hash' is not of type 'str'.") 

604 ex.add_note(f"Got type '{getFullyQualifiedName(hash)}'.") 

605 raise ex 

606 

607 self._parts |= Parts.Hash 

608 self._hash = hash 

609 else: 

610 self._hash = "" 

611 

612 if flags is None: 

613 raise ValueError("Parameter 'flags' is None.") 

614 elif not isinstance(flags, Flags): 

615 ex = TypeError("Parameter 'flags' is not of type 'Flags'.") 

616 ex.add_note(f"Got type '{getFullyQualifiedName(flags)}'.") 

617 raise ex 

618 

619 self._flags = flags 

620 

621 @classmethod 

622 @abstractmethod 

623 def Parse(cls, versionString: Nullable[str], validator: Nullable[Predicate[SemanticVersion]] = None) -> Version: 

624 """ 

625 Parse a version string and return a Version instance. 

626 

627 :param versionString: The version string to parse. 

628 :param validator: Optional, validator rejecting a parsed version, e.g. by word size or maximum value. 

629 :returns: The parsed version number. 

630 """ 

631 

632 @readonly 

633 def Parts(self) -> Parts: 

634 """ 

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

636 

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

638 """ 

639 return self._parts 

640 

641 @readonly 

642 def Prefix(self) -> str: 

643 """ 

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

645 

646 :returns: The prefix of the version number. 

647 """ 

648 return self._prefix 

649 

650 @readonly 

651 def Epoch(self) -> int: 

652 """ 

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

654 

655 An epoch outranks every other part, so a version carrying one is newer than any version with a lower epoch 

656 however high the rest of its numbers are. It exists so a project can *lower* its version number - change 

657 scheme, or recover from a bad release - without every later version comparing as older. A version without one 

658 has epoch ``0``, which is what keeps it comparable with a version that has one. 

659 

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

661 """ 

662 return self._epoch 

663 

664 @readonly 

665 def Major(self) -> int: 

666 """ 

667 Read-only property to access the major number. 

668 

669 :returns: The major number. 

670 """ 

671 return self._major 

672 

673 @readonly 

674 def Minor(self) -> int: 

675 """ 

676 Read-only property to access the minor number. 

677 

678 :returns: The minor number. 

679 """ 

680 return self._minor 

681 

682 @readonly 

683 def Micro(self) -> int: 

684 """ 

685 Read-only property to access the micro number. 

686 

687 :returns: The micro number. 

688 """ 

689 return self._micro 

690 

691 @readonly 

692 def ReleaseLevel(self) -> ReleaseLevel: 

693 """ 

694 Read-only property to access the release level. 

695 

696 :returns: The release level. 

697 """ 

698 return self._releaseLevel 

699 

700 @readonly 

701 def ReleaseNumber(self) -> int: 

702 """ 

703 Read-only property to access the release number. 

704 

705 :returns: The release number. 

706 """ 

707 return self._releaseNumber 

708 

709 @readonly 

710 def Post(self) -> int: 

711 """ 

712 Read-only property to access the post number. 

713 

714 :returns: The post number. 

715 """ 

716 return self._post 

717 

718 @readonly 

719 def Dev(self) -> int: 

720 """ 

721 Read-only property to access the development number. 

722 

723 :returns: The development number. 

724 """ 

725 return self._dev 

726 

727 @readonly 

728 def Build(self) -> int: 

729 """ 

730 Read-only property to access the build number. 

731 

732 :returns: The build number. 

733 """ 

734 return self._build 

735 

736 @readonly 

737 def Postfix(self) -> str: 

738 """ 

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

740 

741 :returns: The postfix of the version number. 

742 """ 

743 return self._postfix 

744 

745 @readonly 

746 def Hash(self) -> str: 

747 """ 

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

749 

750 :returns: The hash. 

751 """ 

752 return self._hash 

753 

754 @readonly 

755 def Flags(self) -> Flags: 

756 """ 

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

758 

759 :returns: The flags of the version number. 

760 """ 

761 return self._flags 

762 

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

764 """ 

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

766 

767 :param left: Left operand. 

768 :param right: Right operand. 

769 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``. 

770 """ 

771 return ( 

772 (left._epoch == right._epoch) and 

773 (left._major == right._major) and 

774 (left._minor == right._minor) and 

775 (left._micro == right._micro) and 

776 (left._releaseLevel == right._releaseLevel) and 

777 (left._releaseNumber == right._releaseNumber) and 

778 ((Parts.Post in left._parts) == (Parts.Post in right._parts)) and 

779 (left._post == right._post) and 

780 ((Parts.Dev in left._parts) == (Parts.Dev in right._parts)) and 

781 (left._dev == right._dev) and 

782 (left._build == right._build) and 

783 (left._postfix == right._postfix) 

784 ) 

785 

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

787 """ 

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

789 

790 :param left: Left operand. 

791 :param right: Right operand. 

792 :returns: ``True``, if ``left`` is smaller than ``right``. |br| 

793 False if ``left`` is greater than ``right``. |br| 

794 Otherwise it's None (both operands are equal). 

795 """ 

796 if left._epoch < right._epoch: 

797 return True 

798 elif left._epoch > right._epoch: 

799 return False 

800 

801 if left._major < right._major: 

802 return True 

803 elif left._major > right._major: 

804 return False 

805 

806 if left._minor < right._minor: 

807 return True 

808 elif left._minor > right._minor: 

809 return False 

810 

811 if left._micro < right._micro: 

812 return True 

813 elif left._micro > right._micro: 

814 return False 

815 

816 if left._releaseLevel < right._releaseLevel: 

817 return True 

818 elif left._releaseLevel > right._releaseLevel: 818 ↛ 819line 818 didn't jump to line 819 because the condition on line 818 was never true

819 return False 

820 

821 if left._releaseNumber < right._releaseNumber: 

822 return True 

823 elif left._releaseNumber > right._releaseNumber: 823 ↛ 824line 823 didn't jump to line 824 because the condition on line 823 was never true

824 return False 

825 

826 leftPost = Parts.Post in left._parts 

827 rightPost = Parts.Post in right._parts 

828 if leftPost != rightPost: 

829 return rightPost 

830 elif left._post < right._post: 830 ↛ 831line 830 didn't jump to line 831 because the condition on line 830 was never true

831 return True 

832 elif left._post > right._post: 832 ↛ 833line 832 didn't jump to line 833 because the condition on line 832 was never true

833 return False 

834 

835 leftDev = Parts.Dev in left._parts 

836 rightDev = Parts.Dev in right._parts 

837 if leftDev != rightDev: 

838 return leftDev 

839 elif left._dev < right._dev: 

840 return True 

841 elif left._dev > right._dev: 

842 return False 

843 

844 if left._build < right._build: 844 ↛ 845line 844 didn't jump to line 845 because the condition on line 844 was never true

845 return True 

846 elif left._build > right._build: 846 ↛ 847line 846 didn't jump to line 847 because the condition on line 846 was never true

847 return False 

848 

849 return None 

850 

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

852 """ 

853 Check if a version fulfills a minimum requirement. 

854 

855 How exact the comparison is depends on how detailed the expected version is: a minor number in the expectation 

856 requires an exact major number, and a micro number requires an exact minor number. 

857 

858 :param actual: The version to check. 

859 :param expected: The minimum version, whose parts decide how exact the comparison is. 

860 :returns: ``True``, if the actual version fulfills the expectation. 

861 """ 

862 exactMajor = Parts.Minor in expected._parts 

863 exactMinor = Parts.Micro in expected._parts 

864 

865 if exactMajor and actual._major != expected._major: 865 ↛ 866line 865 didn't jump to line 866 because the condition on line 865 was never true

866 return False 

867 elif not exactMajor and actual._major < expected._major: 

868 return False 

869 

870 if exactMinor and actual._minor != expected._minor: 870 ↛ 871line 870 didn't jump to line 871 because the condition on line 870 was never true

871 return False 

872 elif not exactMinor and actual._minor < expected._minor: 

873 return False 

874 

875 if Parts.Micro in expected._parts: 

876 return actual._micro >= expected._micro 

877 

878 return True 

879 

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

881 """ 

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

883 

884 .. topic:: Format Specifiers 

885 

886 * ``%p`` - prefix 

887 * ``%M`` - major number 

888 * ``%m`` - minor number 

889 * ``%u`` - micro number 

890 * ``%b`` - build number 

891 

892 :param formatSpec: The format specification. 

893 :returns: Formatted version number. 

894 """ 

895 if formatSpec == "": 

896 return self.__str__() 

897 

898 result = formatSpec 

899 result = result.replace("%p", str(self._prefix)) 

900 result = result.replace("%M", str(self._major)) 

901 result = result.replace("%m", str(self._minor)) 

902 result = result.replace("%u", str(self._micro)) 

903 result = result.replace("%b", str(self._build)) 

904 result = result.replace("%r", str(self._releaseLevel)[0]) 

905 result = result.replace("%R", str(self._releaseLevel)) 

906 result = result.replace("%n", str(self._releaseNumber)) 

907 result = result.replace("%d", str(self._dev)) 

908 result = result.replace("%P", str(self._postfix)) 

909 

910 return result 

911 

912 @mustoverride 

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

914 """ 

915 Compare two version numbers for equality. 

916 

917 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br| 

918 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major 

919 number is assumed (all other parts are zero). 

920 

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

922 number. 

923 

924 :param other: Operand to compare against. 

925 :returns: ``True``, if both version numbers are equal. 

926 :raises ValueError: If parameter ``other`` is None. 

927 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, string or integer. 

928 """ 

929 if other is None: 

930 raise ValueError("Second operand is None.") 

931 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)): 

932 pass 

933 elif isinstance(other, str): 

934 other = self.__class__.Parse(other) 

935 elif isinstance(other, int): 

936 other = self.__class__(major=other) 

937 else: 

938 ex = TypeError("Second operand is not supported by == operator.") 

939 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

940 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, str, int") 

941 raise ex 

942 

943 return self._equal(self, other) 

944 

945 @mustoverride 

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

947 """ 

948 Compare two version numbers for inequality. 

949 

950 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br| 

951 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major 

952 number is assumed (all other parts are zero). 

953 

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

955 number. 

956 

957 :param other: Operand to compare against. 

958 :returns: ``True``, if both version numbers are not equal. 

959 :raises ValueError: If parameter ``other`` is None. 

960 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, string or integer. 

961 """ 

962 if other is None: 

963 raise ValueError("Second operand is None.") 

964 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)): 

965 pass 

966 elif isinstance(other, str): 

967 other = self.__class__.Parse(other) 

968 elif isinstance(other, int): 

969 other = self.__class__(major=other) 

970 else: 

971 ex = TypeError("Second operand is not supported by == operator.") 

972 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

973 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, str, int") 

974 raise ex 

975 

976 return not self._equal(self, other) 

977 

978 @mustoverride 

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

980 """ 

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

982 

983 The second operand should be an instance of :class:`Version`, but :class:`VersionRange`, :class:`VersionSet`, 

984 ``str`` and ``int`` are accepted, too. |br| 

985 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major 

986 number is assumed (all other parts are zero). 

987 

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

989 number. 

990 

991 :param other: Operand to compare against. 

992 :returns: ``True``, if version is less than the second operand. 

993 :raises ValueError: If parameter ``other`` is None. 

994 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, 

995 :class:`VersionSet`, string or integer. 

996 """ 

997 if other is None: 

998 raise ValueError("Second operand is None.") 

999 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)): 

1000 pass 

1001 elif isinstance(other, VersionRange): 

1002 other = other._lowerBound 

1003 elif isinstance(other, VersionSet): 

1004 other = other._items[0] 

1005 elif isinstance(other, str): 

1006 other = self.__class__.Parse(other) 

1007 elif isinstance(other, int): 

1008 other = self.__class__(major=other) 

1009 else: 

1010 ex = TypeError("Second operand is not supported by < operator.") 

1011 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

1012 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, VersionRange, VersionSet, str, int") 

1013 raise ex 

1014 

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

1016 

1017 @mustoverride 

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

1019 """ 

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

1021 

1022 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but 

1023 ``str`` and ``int`` are accepted, too. |br| 

1024 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major 

1025 number is assumed (all other parts are zero). 

1026 

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

1028 number. 

1029 

1030 :param other: Operand to compare against. 

1031 :returns: ``True``, if version is less than or equal the second operand. 

1032 :raises ValueError: If parameter ``other`` is None. 

1033 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, 

1034 :class:`VersionSet`, string or integer. 

1035 """ 

1036 equalValue = True 

1037 if other is None: 

1038 raise ValueError("Second operand is None.") 

1039 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)): 

1040 pass 

1041 elif isinstance(other, VersionRange): 

1042 equalValue = RangeBoundHandling.LowerBoundExclusive not in other._boundHandling 

1043 other = other._lowerBound 

1044 elif isinstance(other, VersionSet): 

1045 other = other._items[0] 

1046 elif isinstance(other, str): 

1047 other = self.__class__.Parse(other) 

1048 elif isinstance(other, int): 

1049 other = self.__class__(major=other) 

1050 else: 

1051 ex = TypeError("Second operand is not supported by <= operator.") 

1052 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

1053 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, VersionRange, VersionSet, str, int") 

1054 raise ex 

1055 

1056 result = self._compare(self, other) 

1057 return result if result is not None else equalValue 

1058 

1059 @mustoverride 

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

1061 """ 

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

1063 

1064 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but 

1065 ``str`` and ``int`` are accepted, too. |br| 

1066 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major 

1067 number is assumed (all other parts are zero). 

1068 

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

1070 number. 

1071 

1072 :param other: Operand to compare against. 

1073 :returns: ``True``, if version is greater than the second operand. 

1074 :raises ValueError: If parameter ``other`` is None. 

1075 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, 

1076 :class:`VersionSet`, string or integer. 

1077 """ 

1078 if other is None: 

1079 raise ValueError("Second operand is None.") 

1080 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)): 

1081 pass 

1082 elif isinstance(other, VersionRange): 

1083 other = other._upperBound 

1084 elif isinstance(other, VersionSet): 

1085 other = other._items[-1] 

1086 elif isinstance(other, str): 

1087 other = self.__class__.Parse(other) 

1088 elif isinstance(other, int): 

1089 other = self.__class__(major=other) 

1090 else: 

1091 ex = TypeError("Second operand is not supported by > operator.") 

1092 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

1093 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, VersionRange, VersionSet, str, int") 

1094 raise ex 

1095 

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

1097 

1098 @mustoverride 

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

1100 """ 

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

1102 

1103 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but 

1104 ``str`` and ``int`` are accepted, too. |br| 

1105 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major 

1106 number is assumed (all other parts are zero). 

1107 

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

1109 number. 

1110 

1111 :param other: Operand to compare against. 

1112 :returns: ``True``, if version is greater than or equal the second operand. 

1113 :raises ValueError: If parameter ``other`` is None. 

1114 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, 

1115 :class:`VersionSet`, string or integer. 

1116 """ 

1117 equalValue = True 

1118 if other is None: 

1119 raise ValueError("Second operand is None.") 

1120 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)): 

1121 pass 

1122 elif isinstance(other, VersionRange): 

1123 equalValue = RangeBoundHandling.UpperBoundExclusive not in other._boundHandling 

1124 other = other._upperBound 

1125 elif isinstance(other, VersionSet): 

1126 other = other._items[-1] 

1127 elif isinstance(other, str): 

1128 other = self.__class__.Parse(other) 

1129 elif isinstance(other, int): 

1130 other = self.__class__(major=other) 

1131 else: 

1132 ex = TypeError("Second operand is not supported by >= operator.") 

1133 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

1134 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, VersionRange, VersionSet, str, int") 

1135 raise ex 

1136 

1137 result = self._compare(self, other) 

1138 return not result if result is not None else equalValue 

1139 

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

1141 """ 

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

1143 

1144 :param other: Second operand, a version, a version string or a major version number. 

1145 :returns: ``True``, if this version is the minimum of both operands. 

1146 :raises ValueError: If the second operand is ``None``. 

1147 :raises TypeError: If the second operand is not a version, a string or an integer. 

1148 """ 

1149 if other is None: 

1150 raise ValueError("Second operand is None.") 

1151 elif isinstance(other, self.__class__): 

1152 pass 

1153 elif isinstance(other, str): 

1154 other = self.__class__.Parse(other) 

1155 elif isinstance(other, int): 

1156 other = self.__class__(major=other) 

1157 else: 

1158 ex = TypeError("Second operand is not supported by >> operator.") 

1159 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

1160 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, str, int") 

1161 raise ex 

1162 

1163 return self._minimum(self, other) 

1164 

1165 def __hash__(self) -> int: 

1166 """ 

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

1168 

1169 All parts of the version are part of the hash, so two versions differing in a postfix or a build number don't 

1170 collide. 

1171 

1172 :returns: Hash of this version number. 

1173 """ 

1174 if self.__hash is None: 

1175 self.__hash = hash(( 

1176 self._prefix, 

1177 self._epoch, 

1178 self._major, 

1179 self._minor, 

1180 self._micro, 

1181 self._releaseLevel, 

1182 self._releaseNumber, 

1183 self._post, 

1184 self._dev, 

1185 self._build, 

1186 self._postfix, 

1187 self._hash, 

1188 self._flags 

1189 )) 

1190 return self.__hash 

1191 

1192 

1193@export 

1194class SemanticVersion(Version): 

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

1196 

1197 _PATTERN: ClassVar[Pattern] = re_compile( 

1198 r"^" 

1199 r"(?P<prefix>rev|REV|[vViIrR])?" 

1200 r"(?:(?P<epoch>\d+):)?" 

1201 r"(?P<major>\d+)" 

1202 r"(?:\.(?P<minor>\d+))?" 

1203 r"(?:\.(?P<micro>\d+))?" 

1204 r"(?:" 

1205 r"(?:\.(?P<build>\d+))" 

1206 r"|" 

1207 r"(?:[\.\-]?(?P<release>dev|final))" 

1208 r"|" 

1209 r"(?:(?P<delim1>[\.\-]?)(?P<level>alpha|beta|gamma|preview|pre|a|b|c|rc|pl)(?P<number>\d*))" 

1210 r")?" 

1211 r"(?:(?P<delim2>[\.\-]post)(?P<post>\d+))?" 

1212 r"(?:(?P<delim3>[\.\-]dev)(?P<dev>\d+))?" 

1213 r"(?:(?P<delim4>[\.\-\+])(?P<postfix>\w+))?" 

1214 r"$" 

1215 ) #: Regular expression to parse a semantic version from a string. 

1216 

1217 #: Spellings of a release level, e.g. ``pre`` and ``preview`` for a release candidate. 

1218 _RELEASE_LEVEL_SPELLINGS: ClassVar[dict[ReleaseLevel, tuple[str, ...]]] = { 

1219 ReleaseLevel.Alpha: ("a", "alpha"), 

1220 ReleaseLevel.Beta: ("b", "beta"), 

1221 ReleaseLevel.Gamma: ("c", "gamma"), 

1222 ReleaseLevel.ReleaseCandidate: ("rc", "pre", "preview") 

1223 } 

1224 

1225 _releaseLevelSpelling: str #: Spelling of the release level as parsed, or ``""`` for the class' own spelling. 

1226# QUESTION: was this how many commits a version is ahead of the last tagged version? 

1227# ahead: int = 0 

1228 

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

1230 """ 

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

1232 

1233 A compiled pattern still carries the expression it was built from, so the base class' one is taken apart and 

1234 put back together with this class' separator. Only the epoch's separator is substituted, and only when the 

1235 class states no pattern of its own - a class replacing the whole expression means it. 

1236 

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

1238 """ 

1239 super().__init_subclass__(**kwargs) 

1240 

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

1242 return 

1243 

1244 cls._PATTERN = re_compile(SemanticVersion._PATTERN.pattern.replace( 

1245 f"(?P<epoch>\\d+){re_escape(SemanticVersion._EPOCH_SEPARATOR)}", 

1246 f"(?P<epoch>\\d+){re_escape(cls._EPOCH_SEPARATOR)}" 

1247 )) 

1248 

1249 def __init__( 

1250 self, 

1251 major: int, 

1252 minor: Nullable[int] = None, 

1253 micro: Nullable[int] = None, 

1254 level: Nullable[ReleaseLevel] = ReleaseLevel.Final, 

1255 number: Nullable[int] = None, 

1256 post: Nullable[int] = None, 

1257 dev: Nullable[int] = None, 

1258 *, 

1259 epoch: Nullable[int] = None, 

1260 build: Nullable[int] = None, 

1261 postfix: Nullable[str] = None, 

1262 prefix: Nullable[str] = None, 

1263 hash: Nullable[str] = None, 

1264 flags: Flags = Flags.NoVCS, 

1265 spelling: Nullable[str] = None 

1266 ) -> None: 

1267 """ 

1268 Initializes a semantic version number representation. 

1269 

1270 :param major: Major number part of the version number. 

1271 :param minor: Optional, minor number part of the version number. 

1272 :param micro: Optional, micro (patch) number part of the version number. 

1273 :param level: Optional, release level of the version number (alpha, beta, release candidate, final, ...). 

1274 :param number: Optional, number within the release level, e.g. ``2`` in ``rc2``. 

1275 :param post: Optional, post number part of the version number. 

1276 :param dev: Optional, development number part of the version number. 

1277 :param epoch: Optional, the version number's epoch, which outranks every other part. 

1278 :param build: Optional, build number part of the version number. 

1279 :param postfix: Optional, the version number's postfix. 

1280 :param prefix: Optional, the version number's prefix. 

1281 :param hash: Optional, hash of the version control system's commit this version was built from. 

1282 :param flags: Optional, the version number's flags. 

1283 :param spelling: Optional, the release level's spelling, e.g. ``pre`` for a release candidate. 

1284 :raises TypeError: If parameter 'major' is not of type integer. 

1285 :raises ValueError: If parameter 'major' is a negative number. 

1286 :raises TypeError: If parameter 'minor' is not of type integer. 

1287 :raises ValueError: If parameter 'minor' is a negative number. 

1288 :raises TypeError: If parameter 'micro' is not of type integer. 

1289 :raises ValueError: If parameter 'micro' is a negative number. 

1290 :raises TypeError: If parameter 'post' is not of type integer. 

1291 :raises ValueError: If parameter 'post' is a negative number. 

1292 :raises TypeError: If parameter 'dev' is not of type integer. 

1293 :raises ValueError: If parameter 'dev' is a negative number. 

1294 :raises TypeError: If parameter 'epoch' is not of type integer. 

1295 :raises ValueError: If parameter 'epoch' is a negative number. 

1296 :raises TypeError: If parameter 'build' is not of type integer. 

1297 :raises ValueError: If parameter 'build' is a negative number. 

1298 :raises TypeError: If parameter 'prefix' is not of type string. 

1299 :raises TypeError: If parameter 'postfix' is not of type string. 

1300 :raises TypeError: If parameter 'spelling' is not of type string. 

1301 :raises ValueError: If parameter 'spelling' isn't a spelling of the release level. 

1302 """ 

1303 super().__init__(major, minor, micro, level, number, post, dev, epoch=epoch, build=build, postfix=postfix, 

1304 prefix=prefix, hash=hash, flags=flags) 

1305 

1306 if spelling is None: 

1307 self._releaseLevelSpelling = "" 

1308 elif not isinstance(spelling, str): 

1309 ex = TypeError("Parameter 'spelling' is not of type 'str'.") 

1310 ex.add_note(f"Got type '{getFullyQualifiedName(spelling)}'.") 

1311 raise ex 

1312 elif spelling not in (spellings := self._RELEASE_LEVEL_SPELLINGS.get(self._releaseLevel, ())): 

1313 ex = ValueError(f"Parameter 'spelling' is '{spelling}', which isn't a spelling of the release level.") 

1314 if len(spellings) == 0: 

1315 ex.add_note(f"Release level '{self._releaseLevel}' has no spelling.") 

1316 else: 

1317 ex.add_note(f"Release level '{self._releaseLevel}' is spelled: {', '.join(spellings)}") 

1318 raise ex 

1319 else: 

1320 self._releaseLevelSpelling = spelling 

1321 

1322 @classmethod 

1323 def Parse(cls, versionString: Nullable[str], validator: Nullable[Predicate[SemanticVersion]] = None) -> SemanticVersion: 

1324 """ 

1325 Parse a version string and return a :class:`SemanticVersion` instance. 

1326 

1327 Allowed prefix characters: 

1328 

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

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

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

1332 * ``rev|REV`` - revision 

1333 

1334 :param versionString: The version string to parse. 

1335 :param validator: Optional, a validation function. 

1336 :returns: An object representing a semantic version. 

1337 :raises TypeError: When parameter ``versionString`` is not a string. 

1338 :raises ValueError: When parameter ``versionString`` is None or empty. 

1339 :raises ValueError: When parameter ``versionString`` isn't a semantic version number. |br| 

1340 It may carry one of the prefixes ``v``, ``i``, ``r`` or ``rev``, e.g. ``v1.2.3``. 

1341 :raises ValueError: When the epoch is malformed. |br| 

1342 An epoch is a number followed by the separator and comes after any prefix, 

1343 e.g. ``2:1.2.3`` or ``v2!1.2.3``. 

1344 :raises VersionValidatorError: When the parsed version is rejected by ``validator``. 

1345 """ 

1346 if versionString is None: 

1347 raise ValueError("Parameter 'versionString' is None.") 

1348 elif not isinstance(versionString, str): 

1349 ex = TypeError("Parameter 'versionString' is not of type 'str'.") 

1350 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.") 

1351 raise ex 

1352 elif (versionString := versionString.strip()) == "": 

1353 raise ValueError("Parameter 'versionString' is empty.") 

1354 

1355 if (match := cls._PATTERN.match(versionString)) is None: 

1356 ex = ValueError(f"Syntax error in parameter 'versionString': '{versionString}'") 

1357 ex.add_note("It may carry one of the prefixes 'v', 'i', 'r' or 'rev', e.g. 'v1.2.3'.") 

1358 raise ex 

1359 

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

1361 """ 

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

1363 

1364 :param value: The matched part, or ``None`` if the pattern didn't match it. 

1365 :returns: The part as an integer, or ``None`` if it wasn't present. 

1366 :raises ValueError: If the part isn't a number. 

1367 """ 

1368 if value is None or value == "": 

1369 return None 

1370 

1371 try: 

1372 return int(value) 

1373 except ValueError as ex: # pragma: no cover 

1374 raise ValueError(f"Invalid part '{value}' in version number '{versionString}'.") from ex 

1375 

1376 prefix = match["prefix"] 

1377 

1378 release = match["release"] 

1379 if release is not None: 

1380 if release == "dev": 1380 ↛ 1382line 1380 didn't jump to line 1382 because the condition on line 1380 was always true

1381 releaseLevel = ReleaseLevel.Development 

1382 elif release == "final": 

1383 releaseLevel = ReleaseLevel.Final 

1384 else: # pragma: no cover 

1385 raise ValueError(f"Unknown release level '{release}' in version number '{versionString}'.") 

1386 else: 

1387 level = match["level"] 

1388 if level is not None: 

1389 level = level.lower() 

1390 if level == "a" or level == "alpha": 

1391 releaseLevel = ReleaseLevel.Alpha 

1392 elif level == "b" or level == "beta": 

1393 releaseLevel = ReleaseLevel.Beta 

1394 elif level == "c" or level == "gamma": 

1395 releaseLevel = ReleaseLevel.Gamma 

1396 elif level == "rc" or level == "pre" or level == "preview": 

1397 releaseLevel = ReleaseLevel.ReleaseCandidate 

1398 else: # pragma: no cover 

1399 raise ValueError(f"Unknown release level '{level}' in version number '{versionString}'.") 

1400 else: 

1401 releaseLevel = ReleaseLevel.Final 

1402 

1403 version = cls( 

1404 major=toInt(match["major"]), 

1405 minor=toInt(match["minor"]), 

1406 micro=toInt(match["micro"]), 

1407 level=releaseLevel, 

1408 number=toInt(match["number"]), 

1409 post=toInt(match["post"]), 

1410 dev=toInt(match["dev"]), 

1411 epoch=toInt(match["epoch"]), 

1412 build=toInt(match["build"]), 

1413 postfix=match["postfix"], 

1414 prefix=prefix if prefix != "" else None, 

1415 # hash=match["hash"], 

1416 flags=Flags.Clean, 

1417 spelling=match["level"] 

1418 ) 

1419 

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

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

1422 

1423 return version 

1424 

1425 @readonly 

1426 def Patch(self) -> int: 

1427 """ 

1428 Read-only property to access the patch number. 

1429 

1430 The patch number is identical to the micro number. 

1431 

1432 :returns: The patch number. 

1433 """ 

1434 return self._micro 

1435 

1436 @readonly 

1437 def ReleaseLevelSpelling(self) -> str: 

1438 """ 

1439 Read-only property to access the release level's spelling. 

1440 

1441 :returns: The spelling as parsed, e.g. ``pre`` for a release candidate, or ``""`` for the class' own spelling. 

1442 """ 

1443 return self._releaseLevelSpelling 

1444 

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

1446 """ 

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

1448 

1449 :param left: Left operand. 

1450 :param right: Right operand. 

1451 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``. 

1452 """ 

1453 return super()._equal(left, right) 

1454 

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

1456 """ 

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

1458 

1459 :param left: Left operand. 

1460 :param right: Right operand. 

1461 :returns: ``True``, if ``left`` is smaller than ``right``. |br| 

1462 False if ``left`` is greater than ``right``. |br| 

1463 Otherwise it's None (both operands are equal). 

1464 """ 

1465 return super()._compare(left, right) 

1466 

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

1468 """ 

1469 Compare two version numbers for equality. 

1470 

1471 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br| 

1472 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major 

1473 number is assumed (all other parts are zero). 

1474 

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

1476 number. 

1477 

1478 :param other: Operand to compare against. 

1479 :returns: ``True``, if both version numbers are equal. 

1480 :raises ValueError: If parameter ``other`` is None. 

1481 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer. 

1482 """ 

1483 return super().__eq__(other) 

1484 

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

1486 """ 

1487 Compare two version numbers for inequality. 

1488 

1489 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br| 

1490 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major 

1491 number is assumed (all other parts are zero). 

1492 

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

1494 number. 

1495 

1496 :param other: Operand to compare against. 

1497 :returns: ``True``, if both version numbers are not equal. 

1498 :raises ValueError: If parameter ``other`` is None. 

1499 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer. 

1500 """ 

1501 return super().__ne__(other) 

1502 

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

1504 """ 

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

1506 

1507 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br| 

1508 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major 

1509 number is assumed (all other parts are zero). 

1510 

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

1512 number. 

1513 

1514 :param other: Operand to compare against. 

1515 :returns: ``True``, if version is less than the second operand. 

1516 :raises ValueError: If parameter ``other`` is None. 

1517 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer. 

1518 """ 

1519 return super().__lt__(other) 

1520 

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

1522 """ 

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

1524 

1525 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br| 

1526 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major 

1527 number is assumed (all other parts are zero). 

1528 

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

1530 number. 

1531 

1532 :param other: Operand to compare against. 

1533 :returns: ``True``, if version is less than or equal the second operand. 

1534 :raises ValueError: If parameter ``other`` is None. 

1535 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer. 

1536 """ 

1537 return super().__le__(other) 

1538 

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

1540 """ 

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

1542 

1543 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br| 

1544 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major 

1545 number is assumed (all other parts are zero). 

1546 

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

1548 number. 

1549 

1550 :param other: Operand to compare against. 

1551 :returns: ``True``, if version is greater than the second operand. 

1552 :raises ValueError: If parameter ``other`` is None. 

1553 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer. 

1554 """ 

1555 return super().__gt__(other) 

1556 

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

1558 """ 

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

1560 

1561 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br| 

1562 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major 

1563 number is assumed (all other parts are zero). 

1564 

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

1566 number. 

1567 

1568 :param other: Operand to compare against. 

1569 :returns: ``True``, if version is greater than or equal the second operand. 

1570 :raises ValueError: If parameter ``other`` is None. 

1571 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer. 

1572 """ 

1573 return super().__ge__(other) 

1574 

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

1576 """ 

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

1578 

1579 :param other: Second operand, a version, a version string or a major version number. 

1580 :returns: ``True``, if this version is the minimum of both operands. 

1581 :raises ValueError: If the second operand is ``None``. 

1582 :raises TypeError: If the second operand is not a version, a string or an integer. 

1583 """ 

1584 return super().__rshift__(other) 

1585 

1586 def __hash__(self) -> int: 

1587 """ 

1588 Compute a hash for this version number. 

1589 

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

1591 version unhashable. 

1592 

1593 :returns: Hash of this version number. 

1594 """ 

1595 return super().__hash__() 

1596 

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

1598 """ 

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

1600 

1601 :param formatSpec: The format specification, using ``%``-placeholders for the version's parts. 

1602 :returns: Formatted version number. 

1603 :raises ValueError: If the format specification contains an unknown placeholder. 

1604 """ 

1605 result = self._format(formatSpec) 

1606 

1607 if (pos := result.find("%")) != -1 and result[pos + 1] != "%": # pragma: no cover 

1608 raise ValueError(f"Unknown format specifier '%{result[pos + 1]}' in '{formatSpec}'.") 

1609 

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

1611 

1612 def __repr__(self) -> str: 

1613 """ 

1614 Return a normalized string representation of this version number. 

1615 

1616 .. note:: 

1617 

1618 A prefix doesn't contribute to the version number's value, therefore it's not part of the normalized form. Use 

1619 :meth:`__str__` to render a version number including its prefix. 

1620 

1621 :returns: Raw version number representation without a prefix. 

1622 """ 

1623 epoch = f"{self._epoch}{self._EPOCH_SEPARATOR}" if Parts.Epoch in self._parts else "" 

1624 

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

1626 

1627 def __str__(self) -> str: 

1628 """ 

1629 Return a string representation of this version number. 

1630 

1631 :returns: Version number representation. 

1632 """ 

1633 result = self._prefix if Parts.Prefix in self._parts else "" 

1634 result += f"{self._epoch}{self._EPOCH_SEPARATOR}" if Parts.Epoch in self._parts else "" 

1635 result += f"{self._major}" # major is always present 

1636 result += f".{self._minor}" if Parts.Minor in self._parts else "" 

1637 result += f".{self._micro}" if Parts.Micro in self._parts else "" 

1638 result += f".{self._build}" if Parts.Build in self._parts else "" 

1639 if self._releaseLevel is ReleaseLevel.Development: 

1640 result += "-dev" 

1641 elif self._releaseLevel is ReleaseLevel.Alpha: 

1642 result += f".{self._releaseLevelSpelling or 'alpha'}{self._releaseNumber}" 

1643 elif self._releaseLevel is ReleaseLevel.Beta: 

1644 result += f".{self._releaseLevelSpelling or 'beta'}{self._releaseNumber}" 

1645 elif self._releaseLevel is ReleaseLevel.Gamma: 1645 ↛ 1646line 1645 didn't jump to line 1646 because the condition on line 1645 was never true

1646 result += f".{self._releaseLevelSpelling or 'gamma'}{self._releaseNumber}" 

1647 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate: 

1648 result += f".{self._releaseLevelSpelling or 'rc'}{self._releaseNumber}" 

1649 result += f".post{self._post}" if Parts.Post in self._parts else "" 

1650 result += f".dev{self._dev}" if Parts.Dev in self._parts else "" 

1651 result += f"+{self._postfix}" if Parts.Postfix in self._parts else "" 

1652 

1653 return result 

1654 

1655 

1656@export 

1657class PythonVersion(SemanticVersion): 

1658 """ 

1659 Represents a Python version. 

1660 """ 

1661 

1662 #: :pep:`440` writes an epoch ``v2!1.2.3``, where Debian and the default write ``2:1.2.3``. 

1663 _EPOCH_SEPARATOR: ClassVar[str] = "!" 

1664 

1665 @classmethod 

1666 def Parse( 

1667 cls, 

1668 versionString: Nullable[str], 

1669 validator: Nullable[Predicate[SemanticVersion]] = None, 

1670 *, 

1671 normalize: bool = False 

1672 ) -> PythonVersion: 

1673 """ 

1674 Parse a version string and return a :class:`PythonVersion` instance. 

1675 

1676 The version keeps the spelling it was parsed from, e.g. ``1.0-pre1`` or ``1.0-dev``, and compares equal to its 

1677 :pep:`440` normalized form ``1.0rc1`` or ``1.0.dev0``. With ``normalize``, the version is normalized as by 

1678 :meth:`Normalize`. 

1679 

1680 :param versionString: The version string to parse. 

1681 :param validator: Optional, a validation function. 

1682 :param normalize: Optional, normalize the parsed version according to :pep:`440`. 

1683 :returns: An object representing a Python version. 

1684 :raises TypeError: When parameter ``versionString`` is not a string. 

1685 :raises ValueError: When parameter ``versionString`` is None or empty. 

1686 :raises ValueError: When parameter ``versionString`` isn't a version number. 

1687 :raises VersionValidatorError: When the parsed version is rejected by ``validator``. 

1688 """ 

1689 version = super().Parse(versionString) 

1690 if normalize: 

1691 version = version.Normalize() 

1692 

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

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

1695 

1696 return version 

1697 

1698 @classmethod 

1699 def FromSysVersionInfo(cls) -> PythonVersion: 

1700 """ 

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

1702 

1703 :returns: A PythonVersion instance of the current Python interpreter's version. 

1704 :raises ToolingException: If the interpreter reports a release level this class doesn't know. 

1705 """ 

1706 if version_info.releaselevel == "final": 

1707 rl = ReleaseLevel.Final 

1708 number = None 

1709 else: # pragma: no cover 

1710 number = version_info.serial 

1711 

1712 if version_info.releaselevel == "alpha": 

1713 rl = ReleaseLevel.Alpha 

1714 elif version_info.releaselevel == "beta": 

1715 rl = ReleaseLevel.Beta 

1716 elif version_info.releaselevel == "candidate": 

1717 rl = ReleaseLevel.ReleaseCandidate 

1718 else: # pragma: no cover 

1719 raise ToolingException(f"Unsupported release level '{version_info.releaselevel}'.") 

1720 

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

1722 

1723 def Normalize(self) -> PythonVersion: 

1724 """ 

1725 Return this version in :pep:`440`'s normalized form. 

1726 

1727 The release candidate's spellings ``c``, ``pre`` and ``preview`` become ``rc``, a development release ``-dev`` 

1728 becomes ``.dev0``, and the prefix is dropped: ``v1.0-pre1`` becomes ``1.0rc1``. 

1729 

1730 :returns: A new version in normalized form. 

1731 """ 

1732 level = self._releaseLevel 

1733 number = self._releaseNumber 

1734 dev = self._dev if Parts.Dev in self._parts else None 

1735 if level is ReleaseLevel.Development: 

1736 level = ReleaseLevel.Final 

1737 dev = 0 

1738 elif level is ReleaseLevel.Gamma: 

1739 level = ReleaseLevel.ReleaseCandidate 

1740 

1741 return self.__class__( 

1742 major=self._major, 

1743 minor=self._minor if Parts.Minor in self._parts else None, 

1744 micro=self._micro if Parts.Micro in self._parts else None, 

1745 level=level, 

1746 number=number if level is not ReleaseLevel.Final else None, 

1747 post=self._post if Parts.Post in self._parts else None, 

1748 dev=dev, 

1749 epoch=self._epoch if Parts.Epoch in self._parts else None, 

1750 build=self._build if Parts.Build in self._parts else None, 

1751 postfix=self._postfix if Parts.Postfix in self._parts else None, 

1752 hash=self._hash if Parts.Hash in self._parts else None, 

1753 flags=self._flags 

1754 ) 

1755 

1756 def _key(self, version: Version) -> tuple[Any, ...]: 

1757 """ 

1758 Private helper method to compute a version's :pep:`440` sort key. 

1759 

1760 A release candidate's spellings are one release level, ``-dev`` is ``.dev0``, and a development release of a 

1761 final release sorts before its pre-releases: ``1.0.dev0 < 1.0a1 < 1.0``. 

1762 

1763 :param version: The version to compute the key for. 

1764 :returns: Tuple of the version's parts in comparison order. 

1765 """ 

1766 level = version._releaseLevel 

1767 number = version._releaseNumber 

1768 hasDev = Parts.Dev in version._parts or level is ReleaseLevel.Development 

1769 if level is ReleaseLevel.Development: 

1770 level = ReleaseLevel.Final 

1771 number = 0 

1772 elif level is ReleaseLevel.Gamma: 

1773 level = ReleaseLevel.ReleaseCandidate 

1774 

1775 post = version._post if Parts.Post in version._parts else -1 

1776 if level is ReleaseLevel.Final and hasDev and post == -1: 

1777 release = (ReleaseLevel.Alpha.value - 1, 0) 

1778 else: 

1779 release = (level.value, number) 

1780 

1781 return ( 

1782 version._epoch, 

1783 version._major, 

1784 version._minor, 

1785 version._micro, 

1786 release, 

1787 post, 

1788 (0, version._dev) if hasDev else (1, 0), 

1789 version._build 

1790 ) 

1791 

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

1793 """ 

1794 Private helper method to compute the equality of two versions according to :pep:`440`. 

1795 

1796 :param left: Left operand. 

1797 :param right: Right operand. 

1798 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``. 

1799 """ 

1800 return self._key(left) == self._key(right) and left._postfix == right._postfix 

1801 

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

1803 """ 

1804 Private helper method to compare two versions according to :pep:`440`. 

1805 

1806 :param left: Left operand. 

1807 :param right: Right operand. 

1808 :returns: ``True``, if ``left`` is smaller than ``right``. |br| 

1809 False if ``left`` is greater than ``right``. |br| 

1810 Otherwise it's None (both operands are equal). 

1811 """ 

1812 leftKey = self._key(left) 

1813 rightKey = self._key(right) 

1814 if leftKey < rightKey: 

1815 return True 

1816 elif leftKey > rightKey: 

1817 return False 

1818 

1819 return None 

1820 

1821 def __hash__(self) -> int: 

1822 """ 

1823 Compute a hash for this version number. 

1824 

1825 Versions comparing equal have equal hashes, so the hash is computed from what :meth:`_equal` compares. 

1826 

1827 :returns: Hash of this version number. 

1828 """ 

1829 return hash((self._key(self), self._postfix)) 

1830 

1831 def __str__(self) -> str: 

1832 """ 

1833 Return a string representation of this version number. 

1834 

1835 :returns: Version number representation. 

1836 """ 

1837 result = self._prefix if Parts.Prefix in self._parts else "" 

1838 result += f"{self._epoch}{self._EPOCH_SEPARATOR}" if Parts.Epoch in self._parts else "" 

1839 result += f"{self._major}" # major is always present 

1840 result += f".{self._minor}" if Parts.Minor in self._parts else "" 

1841 result += f".{self._micro}" if Parts.Micro in self._parts else "" 

1842 result += f".{self._build}" if Parts.Build in self._parts else "" 

1843 if self._releaseLevel is ReleaseLevel.Development: 

1844 result += "-dev" 

1845 elif self._releaseLevel is ReleaseLevel.Alpha: 

1846 result += f"{self._releaseLevelSpelling or 'a'}{self._releaseNumber}" 

1847 elif self._releaseLevel is ReleaseLevel.Beta: 

1848 result += f"{self._releaseLevelSpelling or 'b'}{self._releaseNumber}" 

1849 elif self._releaseLevel is ReleaseLevel.Gamma: 

1850 result += f"{self._releaseLevelSpelling or 'c'}{self._releaseNumber}" 

1851 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate: 

1852 result += f"{self._releaseLevelSpelling or 'rc'}{self._releaseNumber}" 

1853 result += f".post{self._post}" if Parts.Post in self._parts else "" 

1854 result += f".dev{self._dev}" if Parts.Dev in self._parts else "" 

1855 result += f"+{self._postfix}" if Parts.Postfix in self._parts else "" 

1856 

1857 return result 

1858 

1859 

1860@export 

1861class CalendarVersion(Version): 

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

1863 

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

1865 

1866 _PATTERN: ClassVar[Pattern] = re_compile( 

1867 r"^" 

1868 r"(?P<prefix>rev|REV|[vViIrR])?" 

1869 r"(?P<major>\d+)" 

1870 r"(?:\.(?P<minor>\d+))?" 

1871 r"(?:\.(?P<micro>\d+))?" 

1872 r"$" 

1873 ) #: Regular expression to parse a calendar version from a string. 

1874 

1875 def __init__( 

1876 self, 

1877 major: int, 

1878 minor: Nullable[int] = None, 

1879 micro: Nullable[int] = None, 

1880 build: Nullable[int] = None, 

1881 flags: Flags = Flags.Clean, 

1882 prefix: Nullable[str] = None, 

1883 postfix: Nullable[str] = None 

1884 ) -> None: 

1885 """ 

1886 Initializes a calendar version number representation. 

1887 

1888 :param major: Major number part of the version number. 

1889 :param minor: Optional, minor number part of the version number. 

1890 :param micro: Optional, micro (patch) number part of the version number. 

1891 :param build: Optional, build number part of the version number. 

1892 :param flags: Optional, the version number's flags. 

1893 :param prefix: Optional, the version number's prefix. 

1894 :param postfix: Optional, the version number's postfix. 

1895 :raises TypeError: If parameter 'major' is not of type integer. 

1896 :raises ValueError: If parameter 'major' is a negative number. 

1897 :raises TypeError: If parameter 'minor' is not of type integer. 

1898 :raises ValueError: If parameter 'minor' is a negative number. 

1899 :raises TypeError: If parameter 'micro' is not of type integer. 

1900 :raises ValueError: If parameter 'micro' is a negative number. 

1901 :raises TypeError: If parameter 'build' is not of type integer. 

1902 :raises ValueError: If parameter 'build' is a negative number. 

1903 :raises TypeError: If parameter 'prefix' is not of type string. 

1904 :raises TypeError: If parameter 'postfix' is not of type string. 

1905 """ 

1906 super().__init__(major, minor, micro, build=build, postfix=postfix, prefix=prefix, flags=flags) 

1907 

1908 @classmethod 

1909 def Parse(cls, versionString: Nullable[str], validator: Nullable[Predicate[CalendarVersion]] = None) -> CalendarVersion: 

1910 """ 

1911 Parse a version string and return a :class:`CalendarVersion` instance. 

1912 

1913 Allowed prefix characters: 

1914 

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

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

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

1918 * ``rev|REV`` - revision 

1919 

1920 A version number carries up to :attr:`_PARTCOUNT` numeric parts. :class:`YearMonthVersion`, 

1921 :class:`YearWeekVersion` and :class:`YearReleaseVersion` describe two parts, so a third part is rejected for 

1922 them. 

1923 

1924 :param versionString: The version string to parse. 

1925 :param validator: Optional, a validation function. 

1926 :returns: An object representing a calendar version. 

1927 :raises TypeError: If parameter ``versionString`` is not a string. 

1928 :raises ValueError: If parameter ``versionString`` is None or empty. 

1929 :raises ValueError: If parameter ``versionString`` isn't a calendar version number. |br| 

1930 It may carry one of the prefixes ``v``, ``i``, ``r`` or ``rev``, e.g. 

1931 ``v2024.04``. 

1932 :raises ValueError: If parameter ``versionString`` has more parts than the class describes. |br| 

1933 Use :class:`CalendarVersion` or :class:`YearMonthDayVersion` to parse a 

1934 three-part calendar version number. 

1935 :raises VersionValidatorError: If the parsed version is rejected by ``validator``. 

1936 """ 

1937 if versionString is None: 

1938 raise ValueError("Parameter 'versionString' is None.") 

1939 elif not isinstance(versionString, str): 

1940 ex = TypeError("Parameter 'versionString' is not of type 'str'.") 

1941 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.") 

1942 raise ex 

1943 elif (versionString := versionString.strip()) == "": 

1944 raise ValueError("Parameter 'versionString' is empty.") 

1945 

1946 if (match := cls._PATTERN.match(versionString)) is None: 

1947 ex = ValueError(f"Syntax error in parameter 'versionString': '{versionString}'") 

1948 ex.add_note(f"A calendar version number is made of up to {cls._PARTCOUNT} numeric parts, e.g. '2024.04'.") 

1949 ex.add_note("It may carry one of the prefixes 'v', 'i', 'r' or 'rev', e.g. 'v2024.04'.") 

1950 raise ex 

1951 

1952 prefix = match["prefix"] 

1953 minor = match["minor"] 

1954 micro = match["micro"] 

1955 

1956 if micro is not None and cls._PARTCOUNT < 3: 

1957 ex = ValueError(f"Version number '{versionString}' has 3 parts, but '{cls.__name__}' describes {cls._PARTCOUNT}.") 

1958 ex.add_note("Use 'CalendarVersion' or 'YearMonthDayVersion' to parse a 3-part calendar version number.") 

1959 raise ex 

1960 

1961 numbers = [int(match["major"])] 

1962 if minor is not None: 

1963 numbers.append(int(minor)) 

1964 if micro is not None: 

1965 numbers.append(int(micro)) 

1966 

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

1968 

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

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

1971 

1972 return version 

1973 

1974 @readonly 

1975 def Year(self) -> int: 

1976 """ 

1977 Read-only property to access the year part. 

1978 

1979 :returns: The year part. 

1980 """ 

1981 return self._major 

1982 

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

1984 """ 

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

1986 

1987 :param left: Left parameter. 

1988 :param right: Right parameter. 

1989 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``. 

1990 """ 

1991 return (left._major == right._major) and (left._minor == right._minor) and (left._micro == right._micro) 

1992 

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

1994 """ 

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

1996 

1997 :param left: Left parameter. 

1998 :param right: Right parameter. 

1999 :returns: ``True``, if ``left`` is smaller than ``right``. |br| 

2000 False if ``left`` is greater than ``right``. |br| 

2001 Otherwise it's None (both parameters are equal). 

2002 """ 

2003 if left._major < right._major: 

2004 return True 

2005 elif left._major > right._major: 

2006 return False 

2007 

2008 if left._minor < right._minor: 

2009 return True 

2010 elif left._minor > right._minor: 

2011 return False 

2012 

2013 if left._micro < right._micro: 2013 ↛ 2014line 2013 didn't jump to line 2014 because the condition on line 2013 was never true

2014 return True 

2015 elif left._micro > right._micro: 2015 ↛ 2016line 2015 didn't jump to line 2016 because the condition on line 2015 was never true

2016 return False 

2017 

2018 return None 

2019 

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

2021 """ 

2022 Compare two version numbers for equality. 

2023 

2024 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br| 

2025 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major 

2026 number is assumed (all other parts are zero). 

2027 

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

2029 number. 

2030 

2031 :param other: Parameter to compare against. 

2032 :returns: ``True``, if both version numbers are equal. 

2033 :raises ValueError: If parameter ``other`` is None. 

2034 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer. 

2035 """ 

2036 return super().__eq__(other) 

2037 

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

2039 """ 

2040 Compare two version numbers for inequality. 

2041 

2042 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br| 

2043 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major 

2044 number is assumed (all other parts are zero). 

2045 

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

2047 number. 

2048 

2049 :param other: Parameter to compare against. 

2050 :returns: ``True``, if both version numbers are not equal. 

2051 :raises ValueError: If parameter ``other`` is None. 

2052 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer. 

2053 """ 

2054 return super().__ne__(other) 

2055 

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

2057 """ 

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

2059 

2060 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br| 

2061 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major 

2062 number is assumed (all other parts are zero). 

2063 

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

2065 number. 

2066 

2067 :param other: Parameter to compare against. 

2068 :returns: ``True``, if version is less than the second operand. 

2069 :raises ValueError: If parameter ``other`` is None. 

2070 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer. 

2071 """ 

2072 return super().__lt__(other) 

2073 

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

2075 """ 

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

2077 

2078 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br| 

2079 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major 

2080 number is assumed (all other parts are zero). 

2081 

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

2083 number. 

2084 

2085 :param other: Parameter to compare against. 

2086 :returns: ``True``, if version is less than or equal the second operand. 

2087 :raises ValueError: If parameter ``other`` is None. 

2088 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer. 

2089 """ 

2090 return super().__le__(other) 

2091 

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

2093 """ 

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

2095 

2096 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br| 

2097 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major 

2098 number is assumed (all other parts are zero). 

2099 

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

2101 number. 

2102 

2103 :param other: Parameter to compare against. 

2104 :returns: ``True``, if version is greater than the second operand. 

2105 :raises ValueError: If parameter ``other`` is None. 

2106 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer. 

2107 """ 

2108 return super().__gt__(other) 

2109 

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

2111 """ 

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

2113 

2114 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br| 

2115 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major 

2116 number is assumed (all other parts are zero). 

2117 

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

2119 number. 

2120 

2121 :param other: Parameter to compare against. 

2122 :returns: ``True``, if version is greater than or equal the second operand. 

2123 :raises ValueError: If parameter ``other`` is None. 

2124 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer. 

2125 """ 

2126 return super().__ge__(other) 

2127 

2128 def __hash__(self) -> int: 

2129 """ 

2130 Compute a hash for this version number. 

2131 

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

2133 version unhashable. 

2134 

2135 :returns: Hash of this version number. 

2136 """ 

2137 return super().__hash__() 

2138 

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

2140 """ 

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

2142 

2143 .. topic:: Format Specifiers 

2144 

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

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

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

2148 

2149 :param formatSpec: The format specification. 

2150 :returns: Formatted version number. 

2151 """ 

2152 if formatSpec == "": 

2153 return self.__str__() 

2154 

2155 result = formatSpec 

2156 # result = result.replace("%P", str(self._prefix)) 

2157 result = result.replace("%M", str(self._major)) 

2158 result = result.replace("%m", str(self._minor)) 

2159 result = result.replace("%u", str(self._micro)) 

2160 # result = result.replace("%p", str(self._pre)) 

2161 

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

2163 

2164 def __repr__(self) -> str: 

2165 """ 

2166 Return a normalized string representation of this version number. 

2167 

2168 .. note:: 

2169 

2170 A prefix doesn't contribute to the version number's value, therefore it's not part of the normalized form. Use 

2171 :meth:`__str__` to render a version number including its prefix. 

2172 

2173 :returns: Raw version number representation without a prefix. 

2174 """ 

2175 result = f"{self._major}.{self._minor}" 

2176 result += f".{self._micro}" if Parts.Micro in self._parts else "" 

2177 

2178 return result 

2179 

2180 def __str__(self) -> str: 

2181 """ 

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

2183 

2184 :returns: Version number representation including a prefix. 

2185 """ 

2186 result = self._prefix if Parts.Prefix in self._parts else "" 

2187 result += f"{self._major}" 

2188 result += f".{self._minor}" if Parts.Minor in self._parts else "" 

2189 result += f".{self._micro}" if Parts.Micro in self._parts else "" 

2190 

2191 return result 

2192 

2193 

2194@export 

2195class YearMonthVersion(CalendarVersion): 

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

2197 

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

2199 

2200 def __init__( 

2201 self, 

2202 year: int, 

2203 month: Nullable[int] = None, 

2204 build: Nullable[int] = None, 

2205 flags: Flags = Flags.Clean, 

2206 prefix: Nullable[str] = None, 

2207 postfix: Nullable[str] = None 

2208 ) -> None: 

2209 """ 

2210 Initializes a year-month version number representation. 

2211 

2212 :param year: Year part of the version number. 

2213 :param month: Optional, month part of the version number. 

2214 :param build: Optional, build number part of the version number. 

2215 :param flags: Optional, the version number's flags. 

2216 :param prefix: Optional, the version number's prefix. 

2217 :param postfix: Optional, the version number's postfix. 

2218 :raises TypeError: If parameter 'major' is not of type integer. 

2219 :raises ValueError: If parameter 'major' is a negative number. 

2220 :raises TypeError: If parameter 'minor' is not of type integer. 

2221 :raises ValueError: If parameter 'minor' is a negative number. 

2222 :raises TypeError: If parameter 'micro' is not of type integer. 

2223 :raises ValueError: If parameter 'micro' is a negative number. 

2224 :raises TypeError: If parameter 'build' is not of type integer. 

2225 :raises ValueError: If parameter 'build' is a negative number. 

2226 :raises TypeError: If parameter 'prefix' is not of type string. 

2227 :raises TypeError: If parameter 'postfix' is not of type string. 

2228 """ 

2229 super().__init__(year, month, None, build, flags, prefix, postfix) 

2230 

2231 @readonly 

2232 def Month(self) -> int: 

2233 """ 

2234 Read-only property to access the month part. 

2235 

2236 :returns: The month part. 

2237 """ 

2238 return self._minor 

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 

2252@export 

2253class YearWeekVersion(CalendarVersion): 

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

2255 

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

2257 

2258 def __init__( 

2259 self, 

2260 year: int, 

2261 week: Nullable[int] = None, 

2262 build: Nullable[int] = None, 

2263 flags: Flags = Flags.Clean, 

2264 prefix: Nullable[str] = None, 

2265 postfix: Nullable[str] = None 

2266 ) -> None: 

2267 """ 

2268 Initializes a year-week version number representation. 

2269 

2270 :param year: Year part of the version number. 

2271 :param week: Optional, week part of the version number. 

2272 :param build: Optional, build number part of the version number. 

2273 :param flags: Optional, the version number's flags. 

2274 :param prefix: Optional, the version number's prefix. 

2275 :param postfix: Optional, the version number's postfix. 

2276 :raises TypeError: If parameter 'major' is not of type integer. 

2277 :raises ValueError: If parameter 'major' is a negative number. 

2278 :raises TypeError: If parameter 'minor' is not of type integer. 

2279 :raises ValueError: If parameter 'minor' is a negative number. 

2280 :raises TypeError: If parameter 'micro' is not of type integer. 

2281 :raises ValueError: If parameter 'micro' is a negative number. 

2282 :raises TypeError: If parameter 'build' is not of type integer. 

2283 :raises ValueError: If parameter 'build' is a negative number. 

2284 :raises TypeError: If parameter 'prefix' is not of type string. 

2285 :raises TypeError: If parameter 'postfix' is not of type string. 

2286 """ 

2287 super().__init__(year, week, None, build, flags, prefix, postfix) 

2288 

2289 @readonly 

2290 def Week(self) -> int: 

2291 """ 

2292 Read-only property to access the week part. 

2293 

2294 :returns: The week part. 

2295 """ 

2296 return self._minor 

2297 

2298 def __hash__(self) -> int: 

2299 """ 

2300 Compute a hash for this version number. 

2301 

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

2303 version unhashable. 

2304 

2305 :returns: Hash of this version number. 

2306 """ 

2307 return super().__hash__() 

2308 

2309 

2310@export 

2311class YearReleaseVersion(CalendarVersion): 

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

2313 

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

2315 

2316 def __init__( 

2317 self, 

2318 year: int, 

2319 release: Nullable[int] = None, 

2320 build: Nullable[int] = None, 

2321 flags: Flags = Flags.Clean, 

2322 prefix: Nullable[str] = None, 

2323 postfix: Nullable[str] = None 

2324 ) -> None: 

2325 """ 

2326 Initializes a year-release version number representation. 

2327 

2328 :param year: Year part of the version number. 

2329 :param release: Optional, release number of the version number. 

2330 :param build: Optional, build number part of the version number. 

2331 :param flags: Optional, the version number's flags. 

2332 :param prefix: Optional, the version number's prefix. 

2333 :param postfix: Optional, the version number's postfix. 

2334 :raises TypeError: If parameter 'major' is not of type integer. 

2335 :raises ValueError: If parameter 'major' is a negative number. 

2336 :raises TypeError: If parameter 'minor' is not of type integer. 

2337 :raises ValueError: If parameter 'minor' is a negative number. 

2338 :raises TypeError: If parameter 'micro' is not of type integer. 

2339 :raises ValueError: If parameter 'micro' is a negative number. 

2340 :raises TypeError: If parameter 'build' is not of type integer. 

2341 :raises ValueError: If parameter 'build' is a negative number. 

2342 :raises TypeError: If parameter 'prefix' is not of type string. 

2343 :raises TypeError: If parameter 'postfix' is not of type string. 

2344 """ 

2345 super().__init__(year, release, None, build, flags, prefix, postfix) 

2346 

2347 @readonly 

2348 def Release(self) -> int: 

2349 """ 

2350 Read-only property to access the release number. 

2351 

2352 :returns: The release number. 

2353 """ 

2354 return self._minor 

2355 

2356 def __hash__(self) -> int: 

2357 """ 

2358 Compute a hash for this version number. 

2359 

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

2361 version unhashable. 

2362 

2363 :returns: Hash of this version number. 

2364 """ 

2365 return super().__hash__() 

2366 

2367 

2368@export 

2369class YearMonthDayVersion(CalendarVersion): 

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

2371 

2372 def __init__( 

2373 self, 

2374 year: int, 

2375 month: Nullable[int] = None, 

2376 day: Nullable[int] = None, 

2377 build: Nullable[int] = None, 

2378 flags: Flags = Flags.Clean, 

2379 prefix: Nullable[str] = None, 

2380 postfix: Nullable[str] = None 

2381 ) -> None: 

2382 """ 

2383 Initializes a year-month-day version number representation. 

2384 

2385 :param year: Year part of the version number. 

2386 :param month: Optional, month part of the version number. 

2387 :param day: Optional, day part of the version number. 

2388 :param build: Optional, build number part of the version number. 

2389 :param flags: Optional, the version number's flags. 

2390 :param prefix: Optional, the version number's prefix. 

2391 :param postfix: Optional, the version number's postfix. 

2392 :raises TypeError: If parameter 'major' is not of type integer. 

2393 :raises ValueError: If parameter 'major' is a negative number. 

2394 :raises TypeError: If parameter 'minor' is not of type integer. 

2395 :raises ValueError: If parameter 'minor' is a negative number. 

2396 :raises TypeError: If parameter 'micro' is not of type integer. 

2397 :raises ValueError: If parameter 'micro' is a negative number. 

2398 :raises TypeError: If parameter 'build' is not of type integer. 

2399 :raises ValueError: If parameter 'build' is a negative number. 

2400 :raises TypeError: If parameter 'prefix' is not of type string. 

2401 :raises TypeError: If parameter 'postfix' is not of type string. 

2402 """ 

2403 super().__init__(year, month, day, build, flags, prefix, postfix) 

2404 

2405 @readonly 

2406 def Month(self) -> int: 

2407 """ 

2408 Read-only property to access the month part. 

2409 

2410 :returns: The month part. 

2411 """ 

2412 return self._minor 

2413 

2414 @readonly 

2415 def Day(self) -> int: 

2416 """ 

2417 Read-only property to access the day part. 

2418 

2419 :returns: The day part. 

2420 """ 

2421 return self._micro 

2422 

2423 def __hash__(self) -> int: 

2424 """ 

2425 Compute a hash for this version number. 

2426 

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

2428 version unhashable. 

2429 

2430 :returns: Hash of this version number. 

2431 """ 

2432 return super().__hash__() 

2433 

2434 

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

2436 

2437@export 

2438class RangeBoundHandling(Flag): 

2439 """ 

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

2441 

2442 If a bound is inclusive, the bound's value is within the range. If a bound is exclusive, the bound's value is the 

2443 first value outside the range. Inclusive and exclusive behavior can be mixed for lower and upper bounds. 

2444 """ 

2445 BothBoundsInclusive = 0 #: Lower and upper bound are inclusive. 

2446 LowerBoundInclusive = 0 #: Lower bound is inclusive. 

2447 UpperBoundInclusive = 0 #: Upper bound is inclusive. 

2448 LowerBoundExclusive = 1 #: Lower bound is exclusive. 

2449 UpperBoundExclusive = 2 #: Upper bound is exclusive. 

2450 BothBoundsExclusive = 3 #: Lower and upper bound are exclusive. 

2451 

2452 

2453@export 

2454class VersionRange(Generic[V], metaclass=ExtendedType, slots=True): 

2455 """ 

2456 Representation of a version range described by a lower bound and upper bound version. 

2457 

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

2459 

2460 A bound may be **unbound**, written as ``None``, meaning the range is open in that direction. That is how a 

2461 dependency range like Maven's ``[1.0,)`` - *1.0 and everything after it* - is expressed, and it is what makes a 

2462 single comparison such as ``>=1.0`` a range at all. A range unbound at both ends contains every version. 

2463 

2464 Whether the bound itself belongs to the range is :class:`RangeBoundHandling`'s business, not the bound's, so 

2465 each example names the comparison it stands for: 

2466 

2467 .. code-block:: python 

2468 

2469 VersionRange(SemanticVersion.Parse("1.0.0"), None) # >=1.0.0 ⟶ 1.0.0 and everything above it 

2470 VersionRange(None, SemanticVersion.Parse("2.0.0")) # <=2.0.0 ⟶ everything up to 2.0.0 

2471 VersionRange(None, None) # every version 

2472 """ 

2473 _lowerBound: Nullable[V] #: Lower bound of the version range, or ``None`` if it is unbound. 

2474 _upperBound: Nullable[V] #: Upper bound of the version range, or ``None`` if it is unbound. 

2475 _boundHandling: RangeBoundHandling #: Strategy deciding whether the bounds are part of the range. 

2476 

2477 def __init__( 

2478 self, 

2479 lowerBound: Nullable[V], 

2480 upperBound: Nullable[V], 

2481 boundHandling: RangeBoundHandling = RangeBoundHandling.BothBoundsInclusive 

2482 ) -> None: 

2483 """ 

2484 Initializes a version range described by a lower and upper bound. 

2485 

2486 Either bound may be ``None``, which leaves the range open in that direction. The checks that relate the two 

2487 bounds - that they are compatible types, and that the lower one isn't above the upper one - can only be made 

2488 when both are present, so they are skipped for an open bound rather than failing on it. 

2489 

2490 :param lowerBound: Lowest version (inclusive), or ``None`` to leave the range open downwards. 

2491 :param upperBound: Highest version (inclusive), or ``None`` to leave the range open upwards. 

2492 :param boundHandling: Optional, strategy deciding whether the bounds are part of the range. 

2493 :raises TypeError: If parameter ``lowerBound`` is neither ``None`` nor of type :class:`Version`. 

2494 :raises TypeError: If parameter ``upperBound`` is neither ``None`` nor of type :class:`Version`. 

2495 :raises TypeError: If parameter ``lowerBound`` and ``upperBound`` are unrelated types. 

2496 :raises ValueError: If parameter ``lowerBound`` isn't less than or equal to ``upperBound``. 

2497 """ 

2498 if lowerBound is not None and not isinstance(lowerBound, Version): 

2499 ex = TypeError("Parameter 'lowerBound' is not of type 'Version'.") 

2500 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}'.") 

2501 raise ex 

2502 

2503 if upperBound is not None and not isinstance(upperBound, Version): 

2504 ex = TypeError("Parameter 'upperBound' is not of type 'Version'.") 

2505 ex.add_note(f"Got type '{getFullyQualifiedName(upperBound)}'.") 

2506 raise ex 

2507 

2508 if lowerBound is not None and upperBound is not None: 

2509 if not self._AreCompatible(lowerBound, upperBound): 

2510 ex = TypeError("Parameters 'lowerBound' and 'upperBound' are not compatible with each other.") 

2511 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}' for lowerBound and " 

2512 f"type '{getFullyQualifiedName(upperBound)}' for upperBound.") 

2513 raise ex 

2514 

2515 if not (lowerBound <= upperBound): 

2516 ex = ValueError("Parameter 'lowerBound' isn't less than parameter 'upperBound'.") 

2517 ex.add_note(f"Got '{lowerBound}' for lowerBound and '{upperBound}' for upperBound.") 

2518 raise ex 

2519 

2520 self._lowerBound = lowerBound 

2521 self._upperBound = upperBound 

2522 self._boundHandling = boundHandling 

2523 

2524 @property 

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

2526 """ 

2527 Property to access the range's lower bound. 

2528 

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

2530 

2531 :returns: Lower bound of the version range, or ``None`` if it is unbound. 

2532 :raises TypeError: If an assigned value is neither ``None`` nor of type :class:`Version`. 

2533 :raises TypeError: If an assigned value's type is unrelated to the range's upper bound. 

2534 :raises ValueError: If an assigned value is above the range's upper bound. |br| 

2535 The bounds are only related when both are present. 

2536 """ 

2537 return self._lowerBound 

2538 

2539 @LowerBound.setter 

2540 def LowerBound(self, value: Nullable[V]) -> None: 

2541 if value is not None and not isinstance(value, Version): 2541 ↛ 2542line 2541 didn't jump to line 2542 because the condition on line 2541 was never true

2542 ex = TypeError("Parameter 'value' is not of type 'Version'.") 

2543 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

2544 raise ex 

2545 

2546 if value is not None and self._upperBound is not None: 

2547 if not self._AreCompatible(value, self._upperBound): 

2548 ex = TypeError("Parameter 'value' is not compatible with the range's upper bound.") 

2549 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

2550 ex.add_note(f"The upper bound is of type '{getFullyQualifiedName(self._upperBound)}'.") 

2551 raise ex 

2552 

2553 if not (value <= self._upperBound): 

2554 ex = ValueError("Parameter 'value' isn't less than or equal to the range's upper bound.") 

2555 ex.add_note(f"Got '{value}' for the lower bound; the upper bound is '{self._upperBound}'.") 

2556 raise ex 

2557 

2558 self._lowerBound = value 

2559 

2560 @property 

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

2562 """ 

2563 Property to access the range's upper bound. 

2564 

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

2566 

2567 :returns: Upper bound of the version range, or ``None`` if it is unbound. 

2568 :raises TypeError: If an assigned value is neither ``None`` nor of type :class:`Version`. 

2569 :raises TypeError: If an assigned value's type is unrelated to the range's lower bound. 

2570 :raises ValueError: If an assigned value is below the range's lower bound. |br| 

2571 The bounds are only related when both are present. 

2572 """ 

2573 return self._upperBound 

2574 

2575 @UpperBound.setter 

2576 def UpperBound(self, value: Nullable[V]) -> None: 

2577 if value is not None and not isinstance(value, Version): 2577 ↛ 2578line 2577 didn't jump to line 2578 because the condition on line 2577 was never true

2578 ex = TypeError("Parameter 'value' is not of type 'Version'.") 

2579 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

2580 raise ex 

2581 

2582 if value is not None and self._lowerBound is not None: 

2583 if not self._AreCompatible(value, self._lowerBound): 

2584 ex = TypeError("Parameter 'value' is not compatible with the range's lower bound.") 

2585 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

2586 ex.add_note(f"The lower bound is of type '{getFullyQualifiedName(self._lowerBound)}'.") 

2587 raise ex 

2588 

2589 if not (self._lowerBound <= value): 

2590 ex = ValueError("Parameter 'value' isn't greater than or equal to the range's lower bound.") 

2591 ex.add_note(f"Got '{value}' for the upper bound; the lower bound is '{self._lowerBound}'.") 

2592 raise ex 

2593 

2594 self._upperBound = value 

2595 

2596 @property 

2597 def BoundHandling(self) -> RangeBoundHandling: 

2598 """ 

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

2600 

2601 :returns: The range's bound handling strategy. 

2602 :raises TypeError: If an assigned value is not of type :class:`RangeBoundHandling`. 

2603 """ 

2604 return self._boundHandling 

2605 

2606 @BoundHandling.setter 

2607 def BoundHandling(self, value: RangeBoundHandling) -> None: 

2608 if not isinstance(value, RangeBoundHandling): 

2609 ex = TypeError("Parameter 'value' is not of type 'RangeBoundHandling'.") 

2610 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

2611 raise ex 

2612 

2613 self._boundHandling = value 

2614 

2615 @staticmethod 

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

2617 """ 

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

2619 

2620 Two versions relate when they are of the same class, or when one's class derives from the other's. So a 

2621 :class:`SemanticVersion` relates to a :class:`PythonVersion`, which derives from it, and not to a 

2622 :class:`CalendarVersion`, which is a sibling under :class:`Version`. 

2623 

2624 This is the single rule every part of a range applies: to its two bounds against each other, to a version 

2625 held against them, and to a bound assigned after construction. 

2626 

2627 :param left: The first version. 

2628 :param right: The second version. 

2629 :returns: ``True``, if the two versions' types are related. 

2630 """ 

2631 leftType = left.__class__ 

2632 rightType = right.__class__ 

2633 

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

2635 

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

2637 """ 

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

2639 

2640 The rule is the one :meth:`__init__` applies *between* the two bounds: the same class, or one deriving from 

2641 the other. So a range bounded by :class:`SemanticVersion` accepts a :class:`PythonVersion`, because that 

2642 derives from it, and refuses a :class:`CalendarVersion`, which is a sibling. A range whose bounds are both 

2643 unbound carries no type to check against, so it accepts any version. 

2644 

2645 :param other: The version to check against this range's bounds. 

2646 :raises TypeError: If the version's type is unrelated to this range's bounds. 

2647 """ 

2648 reference = self._lowerBound if self._lowerBound is not None else self._upperBound 

2649 if reference is None: 

2650 return 

2651 

2652 if not self._AreCompatible(other, reference): 

2653 ex = TypeError("Parameter 'other' is not compatible with version range.") 

2654 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

2655 ex.add_note(f"This range is bounded by type '{getFullyQualifiedName(reference)}'.") 

2656 raise ex 

2657 

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

2659 """ 

2660 Compute the intersection of two version ranges. 

2661 

2662 Each bound of the result comes from whichever range constrains it more tightly, and is inclusive only if it 

2663 is inclusive in *that* range. Where both ranges name the same bound value, it is inclusive only when **both** 

2664 include it - the intersection cannot admit a version one of its operands excludes. 

2665 

2666 :param other: Second version range to intersect with. 

2667 :returns: Intersected version range. 

2668 :raises TypeError: If parameter 'other' is not of type :class:`VersionRange`. 

2669 :raises TypeError: If the two ranges' bounds are of unrelated types. 

2670 :raises ValueError: If intersection is empty. 

2671 """ 

2672 if not isinstance(other, VersionRange): 2672 ↛ 2673line 2672 didn't jump to line 2673 because the condition on line 2672 was never true

2673 ex = TypeError("Parameter 'other' is not of type 'VersionRange'.") 

2674 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

2675 raise ex 

2676 

2677 if self._lowerBound is not None and other._lowerBound is not None: 

2678 if not self._AreCompatible(self._lowerBound, other._lowerBound): 2678 ↛ 2679line 2678 didn't jump to line 2679 because the condition on line 2678 was never true

2679 ex = TypeError("Parameter 'other's LowerBound and this range's 'LowerBound' are not compatible " 

2680 "with each other.") 

2681 ex.add_note(f"Got type '{getFullyQualifiedName(other._lowerBound)}' for other.LowerBound and " 

2682 f"type '{getFullyQualifiedName(self._lowerBound)}' for self.LowerBound.") 

2683 raise ex 

2684 

2685 ownLowerExclusive = RangeBoundHandling.LowerBoundExclusive in self._boundHandling 

2686 otherLowerExclusive = RangeBoundHandling.LowerBoundExclusive in other._boundHandling 

2687 ownUpperExclusive = RangeBoundHandling.UpperBoundExclusive in self._boundHandling 

2688 otherUpperExclusive = RangeBoundHandling.UpperBoundExclusive in other._boundHandling 

2689 

2690 # An unbound lower end is the lowest of all, so the other range's bound wins; likewise the highest upper one. 

2691 # Each bound keeps the handling of the range it came from; a shared value keeps the stricter of the two. 

2692 if self._lowerBound is None: 

2693 lBound = other._lowerBound 

2694 lowerExclusive = otherLowerExclusive 

2695 elif other._lowerBound is None: 

2696 lBound = self._lowerBound 

2697 lowerExclusive = ownLowerExclusive 

2698 elif self._lowerBound > other._lowerBound: 

2699 lBound = self._lowerBound 

2700 lowerExclusive = ownLowerExclusive 

2701 elif other._lowerBound > self._lowerBound: 

2702 lBound = other._lowerBound 

2703 lowerExclusive = otherLowerExclusive 

2704 else: 

2705 lBound = self._lowerBound 

2706 lowerExclusive = ownLowerExclusive or otherLowerExclusive 

2707 

2708 if self._upperBound is None: 

2709 uBound = other._upperBound 

2710 upperExclusive = otherUpperExclusive 

2711 elif other._upperBound is None: 2711 ↛ 2712line 2711 didn't jump to line 2712 because the condition on line 2711 was never true

2712 uBound = self._upperBound 

2713 upperExclusive = ownUpperExclusive 

2714 elif self._upperBound < other._upperBound: 

2715 uBound = self._upperBound 

2716 upperExclusive = ownUpperExclusive 

2717 elif other._upperBound < self._upperBound: 

2718 uBound = other._upperBound 

2719 upperExclusive = otherUpperExclusive 

2720 else: 

2721 uBound = self._upperBound 

2722 upperExclusive = ownUpperExclusive or otherUpperExclusive 

2723 

2724 if lBound is not None and uBound is not None and not (lBound <= uBound): 

2725 ex = ValueError("The intersection of both version ranges is empty.") 

2726 ex.add_note(f"Got value '{lBound}' for the highest lower bound.") 

2727 ex.add_note(f"The lowest upper bound is '{uBound}'.") 

2728 raise ex 

2729 

2730 boundHandling = RangeBoundHandling.BothBoundsInclusive 

2731 if lowerExclusive: 

2732 boundHandling |= RangeBoundHandling.LowerBoundExclusive 

2733 

2734 if upperExclusive: 

2735 boundHandling |= RangeBoundHandling.UpperBoundExclusive 

2736 

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

2738 

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

2740 """ 

2741 Compare a version range and a version numbers if the version range is less than the second operand (version). 

2742 

2743 :param other: Operand to compare against. 

2744 :returns: ``True``, if version range is less than the second operand (version). 

2745 :raises TypeError: If parameter ``other`` is not of type :class:`Version`. 

2746 """ 

2747 # TODO: support VersionRange < VersionRange too 

2748 # TODO: support str, int, ... like Version ? 

2749 if not isinstance(other, Version): 

2750 ex = TypeError("Parameter 'other' is not of type 'Version'.") 

2751 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

2752 raise ex 

2753 

2754 self._CheckCompatibility(other) 

2755 

2756 if self._upperBound is None: 

2757 return False 

2758 

2759 return self._upperBound < other 

2760 

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

2762 """ 

2763 Compare a version range and a version numbers if the version range is less than or equal the second operand (version). 

2764 

2765 :param other: Operand to compare against. 

2766 :returns: ``True``, if version range is less than or equal the second operand (version). 

2767 :raises TypeError: If parameter ``other`` is not of type :class:`Version`. 

2768 """ 

2769 # TODO: support VersionRange < VersionRange too 

2770 # TODO: support str, int, ... like Version ? 

2771 if not isinstance(other, Version): 2771 ↛ 2772line 2771 didn't jump to line 2772 because the condition on line 2771 was never true

2772 ex = TypeError("Parameter 'other' is not of type 'Version'.") 

2773 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

2774 raise ex 

2775 

2776 self._CheckCompatibility(other) 

2777 

2778 if self._upperBound is None: 

2779 return False 

2780 

2781 if RangeBoundHandling.UpperBoundExclusive in self._boundHandling: 

2782 return self._upperBound < other 

2783 

2784 return self._upperBound <= other 

2785 

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

2787 """ 

2788 Compare a version range and a version numbers if the version range is greater than the second operand (version). 

2789 

2790 :param other: Operand to compare against. 

2791 :returns: ``True``, if version range is greater than the second operand (version). 

2792 :raises TypeError: If parameter ``other`` is not of type :class:`Version`. 

2793 """ 

2794 # TODO: support VersionRange < VersionRange too 

2795 # TODO: support str, int, ... like Version ? 

2796 if not isinstance(other, Version): 2796 ↛ 2797line 2796 didn't jump to line 2797 because the condition on line 2796 was never true

2797 ex = TypeError("Parameter 'other' is not of type 'Version'.") 

2798 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

2799 raise ex 

2800 

2801 self._CheckCompatibility(other) 

2802 

2803 if self._lowerBound is None: 

2804 return False 

2805 

2806 return self._lowerBound > other 

2807 

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

2809 """ 

2810 Compare a version range and a version numbers if the version range is greater than or equal the second operand (version). 

2811 

2812 :param other: Operand to compare against. 

2813 :returns: ``True``, if version range is greater than or equal the second operand (version). 

2814 :raises TypeError: If parameter ``other`` is not of type :class:`Version`. 

2815 """ 

2816 # TODO: support VersionRange < VersionRange too 

2817 # TODO: support str, int, ... like Version ? 

2818 if not isinstance(other, Version): 2818 ↛ 2819line 2818 didn't jump to line 2819 because the condition on line 2818 was never true

2819 ex = TypeError("Parameter 'other' is not of type 'Version'.") 

2820 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

2821 raise ex 

2822 

2823 self._CheckCompatibility(other) 

2824 

2825 if self._lowerBound is None: 

2826 return False 

2827 

2828 if RangeBoundHandling.LowerBoundExclusive in self._boundHandling: 2828 ↛ 2829line 2828 didn't jump to line 2829 because the condition on line 2828 was never true

2829 return self._lowerBound > other 

2830 

2831 return self._lowerBound >= other 

2832 

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

2834 """ 

2835 Check if the version is in the version range. 

2836 

2837 :param version: Version to check. 

2838 :returns: ``True``, if version is in range. 

2839 :raises TypeError: If parameter ``version`` is not of type :class:`Version`. 

2840 :raises TypeError: If parameter ``version``'s type is unrelated to this range's bounds. |br| 

2841 The rule is the one :meth:`__init__` applies between the bounds. 

2842 """ 

2843 if not isinstance(version, Version): 2843 ↛ 2844line 2843 didn't jump to line 2844 because the condition on line 2843 was never true

2844 ex = TypeError("Parameter 'item' is not of type 'Version'.") 

2845 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

2846 raise ex 

2847 

2848 self._CheckCompatibility(version) 

2849 

2850 # An unbound end excludes nothing, so its half of the comparison is simply not made. 

2851 if self._lowerBound is not None: 

2852 if RangeBoundHandling.LowerBoundExclusive in self._boundHandling: 

2853 if not (self._lowerBound < version): 

2854 return False 

2855 elif not (self._lowerBound <= version): 

2856 return False 

2857 

2858 if self._upperBound is not None: 

2859 if RangeBoundHandling.UpperBoundExclusive in self._boundHandling: 

2860 if not (version < self._upperBound): 

2861 return False 

2862 elif not (version <= self._upperBound): 

2863 return False 

2864 

2865 return True 

2866 

2867 

2868@export 

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

2870 """ 

2871 Representation of an ordered set of versions. 

2872 

2873 This version set works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes. 

2874 """ 

2875 _items: list[V] #: An ordered list of set members. 

2876 

2877 def __init__(self, versions: Union[Version, Iterable[V]]) -> None: 

2878 """ 

2879 Initializes a version set either by a single version or an iterable of versions. 

2880 

2881 :param versions: A single version or an iterable of versions. 

2882 :raises ValueError: If parameter ``versions`` is None`. 

2883 :raises TypeError: In case of a single version, if parameter ``version`` is not of type :class:`Version`. 

2884 :raises TypeError: In case of an iterable, if parameter ``versions`` containes elements, which are not of type :class:`Version`. 

2885 :raises TypeError: If parameter ``versions`` is neither a single version nor an iterable thereof. 

2886 """ 

2887 if versions is None: 

2888 raise ValueError("Parameter 'versions' is None.") 

2889 

2890 if isinstance(versions, Version): 

2891 self._items = [versions] 

2892 elif isinstance(versions, abc_Iterable): 2892 ↛ 2910line 2892 didn't jump to line 2910 because the condition on line 2892 was always true

2893 iterator = iter(versions) 

2894 try: 

2895 firstVersion = next(iterator) 

2896 except StopIteration: 

2897 self._items = [] 

2898 return 

2899 

2900 if not isinstance(firstVersion, Version): 2900 ↛ 2901line 2900 didn't jump to line 2901 because the condition on line 2900 was never true

2901 raise TypeError("First element in parameter 'versions' is not of type Version.") 

2902 

2903 baseType = firstVersion.__class__ 

2904 for version in iterator: 

2905 if not isinstance(version, baseType): 

2906 raise TypeError(f"Element from parameter 'versions' is not of type {baseType.__name__}") 

2907 

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

2909 else: 

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

2911 

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

2913 """ 

2914 Compute intersection of two version sets. 

2915 

2916 :param other: Second set of versions. 

2917 :returns: Intersection of two version sets. 

2918 """ 

2919 selfIterator = self.__iter__() 

2920 otherIterator = other.__iter__() 

2921 

2922 result = [] 

2923 try: 

2924 selfValue = next(selfIterator) 

2925 otherValue = next(otherIterator) 

2926 

2927 while True: 

2928 if selfValue < otherValue: 

2929 selfValue = next(selfIterator) 

2930 elif otherValue < selfValue: 

2931 otherValue = next(otherIterator) 

2932 else: 

2933 result.append(selfValue) 

2934 selfValue = next(selfIterator) 

2935 otherValue = next(otherIterator) 

2936 

2937 except StopIteration: 

2938 pass 

2939 

2940 return VersionSet(result) 

2941 

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

2943 """ 

2944 Compute union of two version sets. 

2945 

2946 :param other: Second set of versions. 

2947 :returns: Union of two version sets. 

2948 """ 

2949 selfIterator = self.__iter__() 

2950 otherIterator = other.__iter__() 

2951 

2952 result = [] 

2953 try: 

2954 selfValue = next(selfIterator) 

2955 except StopIteration: 

2956 for otherValue in otherIterator: 

2957 result.append(otherValue) 

2958 

2959 try: 

2960 otherValue = next(otherIterator) 

2961 except StopIteration: 

2962 for selfValue in selfIterator: 

2963 result.append(selfValue) 

2964 

2965 while True: 

2966 if selfValue < otherValue: 

2967 result.append(selfValue) 

2968 try: 

2969 selfValue = next(selfIterator) 

2970 except StopIteration: 

2971 result.append(otherValue) 

2972 for otherValue in otherIterator: 2972 ↛ 2973line 2972 didn't jump to line 2973 because the loop on line 2972 never started

2973 result.append(otherValue) 

2974 

2975 break 

2976 elif otherValue < selfValue: 

2977 result.append(otherValue) 

2978 try: 

2979 otherValue = next(otherIterator) 

2980 except StopIteration: 

2981 result.append(selfValue) 

2982 for selfValue in selfIterator: 

2983 result.append(selfValue) 

2984 

2985 break 

2986 else: 

2987 result.append(selfValue) 

2988 try: 

2989 selfValue = next(selfIterator) 

2990 except StopIteration: 

2991 for otherValue in otherIterator: 2991 ↛ 2992line 2991 didn't jump to line 2992 because the loop on line 2991 never started

2992 result.append(otherValue) 

2993 

2994 break 

2995 

2996 try: 

2997 otherValue = next(otherIterator) 

2998 except StopIteration: 

2999 for selfValue in selfIterator: 

3000 result.append(selfValue) 

3001 

3002 break 

3003 

3004 return VersionSet(result) 

3005 

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

3007 """ 

3008 Compare a version set and a version numbers if the version set is less than the second operand (version). 

3009 

3010 :param other: Operand to compare against. 

3011 :returns: ``True``, if version set is less than the second operand (version). 

3012 :raises TypeError: If parameter ``other`` is not of type :class:`Version`. 

3013 """ 

3014 # TODO: support VersionRange < VersionRange too 

3015 # TODO: support str, int, ... like Version ? 

3016 if not isinstance(other, Version): 

3017 ex = TypeError("Parameter 'other' is not of type 'Version'.") 

3018 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

3019 raise ex 

3020 

3021 return self._items[-1] < other 

3022 

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

3024 """ 

3025 Compare a version set and a version numbers if the version set is less than or equal the second operand (version). 

3026 

3027 :param other: Operand to compare against. 

3028 :returns: ``True``, if version set is less than or equal the second operand (version). 

3029 :raises TypeError: If parameter ``other`` is not of type :class:`Version`. 

3030 """ 

3031 # TODO: support VersionRange < VersionRange too 

3032 # TODO: support str, int, ... like Version ? 

3033 if not isinstance(other, Version): 3033 ↛ 3034line 3033 didn't jump to line 3034 because the condition on line 3033 was never true

3034 ex = TypeError("Parameter 'other' is not of type 'Version'.") 

3035 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

3036 raise ex 

3037 

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

3039 

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

3041 """ 

3042 Compare a version set and a version numbers if the version set is greater than the second operand (version). 

3043 

3044 :param other: Operand to compare against. 

3045 :returns: ``True``, if version set is greater than the second operand (version). 

3046 :raises TypeError: If parameter ``other`` is not of type :class:`Version`. 

3047 """ 

3048 # TODO: support VersionRange < VersionRange too 

3049 # TODO: support str, int, ... like Version ? 

3050 if not isinstance(other, Version): 3050 ↛ 3051line 3050 didn't jump to line 3051 because the condition on line 3050 was never true

3051 ex = TypeError("Parameter 'other' is not of type 'Version'.") 

3052 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

3053 raise ex 

3054 

3055 return self._items[0] > other 

3056 

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

3058 """ 

3059 Compare a version set and a version numbers if the version set is greater than or equal the second operand (version). 

3060 

3061 :param other: Operand to compare against. 

3062 :returns: ``True``, if version set is greater than or equal the second operand (version). 

3063 :raises TypeError: If parameter ``other`` is not of type :class:`Version`. 

3064 """ 

3065 # TODO: support VersionRange < VersionRange too 

3066 # TODO: support str, int, ... like Version ? 

3067 if not isinstance(other, Version): 3067 ↛ 3068line 3067 didn't jump to line 3068 because the condition on line 3067 was never true

3068 ex = TypeError("Parameter 'other' is not of type 'Version'.") 

3069 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

3070 raise ex 

3071 

3072 return self._items[0] >= other 

3073 

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

3075 """ 

3076 Checks if the version a member of the set. 

3077 

3078 :param version: The version to check. 

3079 :returns: ``True``, if the version is a member of the set. 

3080 """ 

3081 return version in self._items 

3082 

3083 def __len__(self) -> int: 

3084 """ 

3085 Returns the number of members in the set. 

3086 

3087 :returns: Number of set members. 

3088 """ 

3089 return len(self._items) 

3090 

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

3092 """ 

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

3094 

3095 :returns: Iterator to iterate versions. 

3096 """ 

3097 return self._items.__iter__() 

3098 

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

3100 """ 

3101 Access to a version of a set by index. 

3102 

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

3104 :returns: The indexed version. 

3105 

3106 .. hint:: 

3107 

3108 Versions are ordered from lowest to highest version number. 

3109 """ 

3110 return self._items[index] 

3111 

3112 

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

3114_VersionType = Version 

3115 

3116 

3117@export 

3118class VersionComparison(Enum): 

3119 """ 

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

3121 

3122 The six ordering comparisons mean the same thing in every packaging ecosystem, even where the spelling differs - 

3123 Debian writes ``<<`` for :attr:`LessThan` and npm writes ``=`` for :attr:`Equal`. Each member's value is its 

3124 *canonical* spelling, which is what a constraint renders as; a dialect maps its own spellings onto these members 

3125 while parsing. 

3126 """ 

3127 

3128 Equal = "==" #: The version has to be equal to the constraint's version. 

3129 Unequal = "!=" #: The version must not be the constraint's version. 

3130 LessThan = "<" #: The version has to be lower than the constraint's version. 

3131 LessThanOrEqual = "<=" #: The version must not be higher than the constraint's version. 

3132 GreaterThan = ">" #: The version has to be higher than the constraint's version. 

3133 GreaterThanOrEqual = ">=" #: The version must not be lower than the constraint's version. 

3134 CompatibleRelease = "~=" #: The version has to be compatible with the constraint's version (:pep:`440`). 

3135 Caret = "^" #: The version must not change the constraint's leftmost non-zero part (npm). 

3136 Tilde = "~" #: The version must not change the constraint's minor part (npm). 

3137 

3138 def __str__(self) -> str: 

3139 """ 

3140 Return the operator in its canonical spelling. 

3141 

3142 :returns: The comparison's operator. 

3143 """ 

3144 return self.value 

3145 

3146 

3147@export 

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

3149 """ 

3150 One comparison of a :class:`VersionExpression`. 

3151 

3152 A constraint is written e.g. as ``>=1.2.0``. It is a container of the versions satisfying it, so membership is asked 

3153 with ``in``: 

3154 

3155 .. code-block:: python 

3156 

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

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

3159 

3160 .. seealso:: 

3161 

3162 :class:`CompatibleVersionConstraint` 

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

3164 """ 

3165 

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

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

3168 

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

3170 """ 

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

3172 

3173 :param comparison: The comparison to apply. 

3174 :param version: The version to compare against. 

3175 :raises TypeError: If parameter 'comparison' is not of type :class:`VersionComparison`. 

3176 :raises TypeError: If parameter 'version' is not of type :class:`Version`. 

3177 :raises ValueError: If parameter 'comparison' is a shorthand like 

3178 :attr:`~VersionComparison.CompatibleRelease`. |br| 

3179 Use the matching :class:`RangeVersionConstraint`, which derives an upper bound from the 

3180 version. 

3181 """ 

3182 if not isinstance(comparison, VersionComparison): 

3183 ex = TypeError("Parameter 'comparison' is not of type 'VersionComparison'.") 

3184 ex.add_note(f"Got type '{getFullyQualifiedName(comparison)}'.") 

3185 raise ex 

3186 

3187 if not isinstance(version, Version): 

3188 ex = TypeError("Parameter 'version' is not of type 'Version'.") 

3189 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

3190 raise ex 

3191 

3192 if comparison in _SHORTHAND_COMPARISONS and not isinstance(self, RangeVersionConstraint): 

3193 ex = ValueError(f"Comparison '{comparison.name}' is not a plain comparison.") 

3194 ex.add_note("Use the matching 'RangeVersionConstraint', which derives an upper bound from the version.") 

3195 raise ex 

3196 

3197 self._comparison = comparison 

3198 self._version = version 

3199 

3200 @readonly 

3201 def Comparison(self) -> VersionComparison: 

3202 """ 

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

3204 

3205 :returns: The comparison. 

3206 """ 

3207 return self._comparison 

3208 

3209 @readonly 

3210 def Version(self) -> V: 

3211 """ 

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

3213 

3214 :returns: The version. 

3215 """ 

3216 return self._version 

3217 

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

3219 """ 

3220 Check if a version satisfies this constraint. 

3221 

3222 :param version: The version to check. 

3223 :returns: ``True``, if the version satisfies the constraint. 

3224 :raises TypeError: If parameter 'version' is not of type :class:`Version`. 

3225 :raises ValueError: If this constraint carries a comparison a plain constraint cannot apply. |br| 

3226 A shorthand is implemented by a :class:`RangeVersionConstraint`. 

3227 """ 

3228 if not isinstance(version, Version): 3228 ↛ 3229line 3228 didn't jump to line 3229 because the condition on line 3228 was never true

3229 ex = TypeError("Parameter 'version' is not of type 'Version'.") 

3230 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

3231 raise ex 

3232 

3233 if self._comparison is VersionComparison.Equal: 

3234 return version == self._version 

3235 elif self._comparison is VersionComparison.Unequal: 

3236 return version != self._version 

3237 elif self._comparison is VersionComparison.LessThan: 

3238 return version < self._version 

3239 elif self._comparison is VersionComparison.LessThanOrEqual: 

3240 return version <= self._version 

3241 elif self._comparison is VersionComparison.GreaterThan: 

3242 return version > self._version 

3243 elif self._comparison is VersionComparison.GreaterThanOrEqual: 

3244 return version >= self._version 

3245 

3246 ex = ValueError(f"Comparison '{self._comparison.name}' cannot be applied by a plain constraint.") 

3247 ex.add_note("A shorthand is implemented by a 'RangeVersionConstraint'.") 

3248 raise ex 

3249 

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

3251 """ 

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

3253 

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

3255 

3256 * ``>=1.2.0`` is ``[1.2.0, )``, 

3257 * ``>1.2.0`` is ``(1.2.0, )``, 

3258 * ``<=2.0.0`` is ``( , 2.0.0]``, 

3259 * ``<2.0.0`` is ``( , 2.0.0)``, 

3260 * ``==1.2.0`` is ``[1.2.0, 1.2.0]``, a range of exactly one version. 

3261 

3262 :attr:`~VersionComparison.Unequal` is the exception and has no range: the complement of a single version is 

3263 not an interval but a *union* of two, one below it and one above. That is why an expression keeps both 

3264 representations rather than being replaced by a range. 

3265 

3266 :returns: The range of versions satisfying this constraint. 

3267 :raises ValueError: If this constraint is an :attr:`~VersionComparison.Unequal`, which no single range 

3268 describes. |br| 

3269 The complement of a version is a union of two intervals. 

3270 """ 

3271 if self._comparison is VersionComparison.Equal: 

3272 return VersionRange(self._version, self._version) 

3273 elif self._comparison is VersionComparison.GreaterThanOrEqual: 

3274 return VersionRange(self._version, None) 

3275 elif self._comparison is VersionComparison.GreaterThan: 

3276 return VersionRange(self._version, None, RangeBoundHandling.LowerBoundExclusive) 

3277 elif self._comparison is VersionComparison.LessThanOrEqual: 

3278 return VersionRange(None, self._version) 

3279 elif self._comparison is VersionComparison.LessThan: 

3280 return VersionRange(None, self._version, RangeBoundHandling.UpperBoundExclusive) 

3281 

3282 ex = ValueError(f"Comparison '{self._comparison.name}' describes no single version range.") 

3283 ex.add_note("The complement of a version is a union of two intervals, which a 'VersionRange' cannot hold.") 

3284 raise ex 

3285 

3286 def __str__(self) -> str: 

3287 """ 

3288 Return the constraint in its canonical spelling. 

3289 

3290 :returns: The operator followed by the version. 

3291 """ 

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

3293 

3294 

3295#: The comparisons a plain :class:`VersionConstraint` cannot express, because each derives an upper bound. 

3296_SHORTHAND_COMPARISONS = ( 

3297 VersionComparison.CompatibleRelease, 

3298 VersionComparison.Caret, 

3299 VersionComparison.Tilde, 

3300) 

3301 

3302 

3303@export 

3304class RangeVersionConstraint(VersionConstraint[V]): 

3305 """ 

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

3307 

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

3309 rule: 

3310 

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

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

3313 * RubyGems writes ``~>``. 

3314 

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

3316 

3317 .. seealso:: 

3318 

3319 :class:`CompatibleVersionConstraint` 

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

3321 :class:`CaretVersionConstraint` 

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

3323 :class:`TildeVersionConstraint` 

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

3325 """ 

3326 

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

3328 

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

3330 """ 

3331 Initialize a shorthand constraint and derive its upper bound. 

3332 

3333 :param comparison: The shorthand comparison this constraint applies. 

3334 :param version: The version the shorthand is written with. 

3335 :raises TypeError: If parameter 'version' is not of type :class:`SemanticVersion`. |br| 

3336 An upper bound is derived from the version's parts, which only a semantic version has. 

3337 :raises ValueError: If the version has too few parts for this shorthand. 

3338 """ 

3339 if not isinstance(version, SemanticVersion): 

3340 ex = TypeError("Parameter 'version' is not of type 'SemanticVersion'.") 

3341 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

3342 raise ex 

3343 

3344 super().__init__(comparison, version) 

3345 

3346 self._upperBound = self._DeriveUpperBound(version) 

3347 

3348 @abstractmethod 

3349 def _DeriveUpperBound(self, version: SemanticVersion) -> SemanticVersion: # type: ignore[empty-body] 

3350 """ 

3351 Derive the first version outside this constraint from the version it is written with. 

3352 

3353 The bound has to be built in the written version's **epoch**. A bound left in epoch 0 outranks nothing, so 

3354 the constraint would match no version at all - not even the one it was written with. 

3355 

3356 :param version: The version the shorthand is written with, always a semantic version. 

3357 :returns: The exclusive upper bound, in the same epoch as ``version``. 

3358 :raises ValueError: If the version has too few parts for this shorthand. 

3359 """ 

3360 

3361 @readonly 

3362 def UpperBound(self) -> SemanticVersion: 

3363 """ 

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

3365 

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

3367 """ 

3368 return self._upperBound 

3369 

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

3371 """ 

3372 Check if a version is within this constraint. 

3373 

3374 :param version: The version to check. 

3375 :returns: ``True``, if the version is at least the written one and below the derived upper bound. 

3376 :raises TypeError: If parameter 'version' is not of type :class:`Version`. 

3377 """ 

3378 if not isinstance(version, Version): 3378 ↛ 3379line 3378 didn't jump to line 3379 because the condition on line 3378 was never true

3379 ex = TypeError("Parameter 'version' is not of type 'Version'.") 

3380 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

3381 raise ex 

3382 

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

3384 

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

3386 """ 

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

3388 

3389 A shorthand constraint knows both of its bounds - the written version is the inclusive lower one, the derived 

3390 bound the exclusive upper one - so the conversion is exact and loses nothing. 

3391 

3392 :returns: The range of versions satisfying this constraint. 

3393 """ 

3394 return VersionRange( 

3395 self._version, 

3396 self._upperBound, 

3397 RangeBoundHandling.LowerBoundInclusive | RangeBoundHandling.UpperBoundExclusive 

3398 ) 

3399 

3400 

3401@export 

3402class CompatibleVersionConstraint(RangeVersionConstraint[V]): 

3403 """ 

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

3405 

3406 The last part written may move, the one to its left may not: ``~=1.2.3`` is ``<1.3``, ``~=1.2`` is ``<2`` and 

3407 ``~=1.2.3.4`` is ``<1.2.4``. The written version therefore needs at least two parts - ``~=1`` would say nothing 

3408 that ``>=1`` doesn't, and :pep:`440` rejects it for that reason. 

3409 

3410 .. code-block:: python 

3411 

3412 constraint = CompatibleVersionConstraint(PythonVersion.Parse("1.2.3")) 

3413 PythonVersion.Parse("1.2.9") in constraint # True 

3414 PythonVersion.Parse("1.3.0") in constraint # False 

3415 """ 

3416 

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

3418 """ 

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

3420 

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

3422 """ 

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

3424 

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

3426 """ 

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

3428 

3429 :param version: The version the shorthand is written with. 

3430 :returns: The exclusive upper bound. 

3431 :raises ValueError: If the version has fewer than two parts. 

3432 """ 

3433 versionType = version.__class__ 

3434 epoch = version.Epoch if Parts.Epoch in version._parts else None 

3435 if Parts.Build in version._parts: 

3436 return versionType(version.Major, version.Minor, version.Patch + 1, epoch=epoch) 

3437 elif Parts.Micro in version._parts: 

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

3439 elif Parts.Minor in version._parts: 

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

3441 

3442 ex = ValueError(f"Version '{version}' has too few parts for a compatible release.") 

3443 ex.add_note("'~=1' would mean the same as '>=1'; write at least a major and a minor part.") 

3444 raise ex 

3445 

3446 

3447@export 

3448class CaretVersionConstraint(RangeVersionConstraint[V]): 

3449 """ 

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

3451 

3452 ``^1.2.3`` is ``<2.0.0``, but ``^0.2.3`` is ``<0.3.0`` and ``^0.0.3`` is ``<0.0.4`` - below 1.0.0 npm treats 

3453 each part as breaking, which is what makes this different from :class:`TildeVersionConstraint`. A part that was 

3454 not written cannot be the pivot, so ``^0`` is ``<1.0.0`` while ``^0.0`` is ``<0.1.0``. 

3455 

3456 .. note:: 

3457 

3458 npm excludes pre-releases of the upper bound by writing it ``<2.0.0-0``. That distinction is not modelled 

3459 here; a pre-release of the bound is compared by :class:`SemanticVersion`'s own ordering. 

3460 """ 

3461 

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

3463 """ 

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

3465 

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

3467 """ 

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

3469 

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

3471 """ 

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

3473 

3474 :param version: The version the shorthand is written with. 

3475 :returns: The exclusive upper bound. 

3476 """ 

3477 versionType = version.__class__ 

3478 epoch = version.Epoch if Parts.Epoch in version._parts else None 

3479 if version.Major != 0 or Parts.Minor not in version._parts: 

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

3481 elif version.Minor != 0 or Parts.Micro not in version._parts: 

3482 return versionType(0, version.Minor + 1, epoch=epoch) 

3483 

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

3485 

3486 

3487@export 

3488class TildeVersionConstraint(RangeVersionConstraint[V]): 

3489 """ 

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

3491 

3492 ``~1.2.3`` and ``~1.2`` are both ``<1.3.0``. Only when no minor part was written does the major one become the 

3493 pivot, so ``~1`` is ``<2.0.0``. 

3494 

3495 This is **not** :pep:`440`'s ``~=``: the two agree on ``~1.2.3`` and disagree on ``~1.2``, which npm reads as 

3496 ``<1.3.0`` and :pep:`440` as ``<2``. 

3497 """ 

3498 

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

3500 """ 

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

3502 

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

3504 """ 

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

3506 

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

3508 """ 

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

3510 

3511 :param version: The version the shorthand is written with. 

3512 :returns: The exclusive upper bound. 

3513 """ 

3514 versionType = version.__class__ 

3515 epoch = version.Epoch if Parts.Epoch in version._parts else None 

3516 if Parts.Minor in version._parts: 

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

3518 

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

3520 

3521 

3522@export 

3523def _BuildConstraintPattern( 

3524 operators: dict[str, VersionComparison], 

3525 separators: str, 

3526 versionType: type[Version] 

3527) -> Pattern[str]: 

3528 """ 

3529 Build the pattern matching one constraint of a :class:`VersionExpression` dialect. 

3530 

3531 The operators are alternated longest-first, so ``>=`` wins over ``>`` and ``~=`` over any single character. The 

3532 operator and its version are matched *together*, with whitespace allowed between them, which is what lets 

3533 whitespace separate constraints without splitting ``>= 1.2.0`` in half. 

3534 

3535 A version may hold none of the characters the dialect's operators are built from, and none of its separators, so 

3536 those are excluded from it. Without that the optional operator group would let ``>=1.2.0 <2.0.0`` read its second 

3537 constraint as the *version* ``<2.0.0``. 

3538 

3539 The **epoch separator is the exception** and stays allowed: :pep:`440` writes an epoch ``1!1.0``, and ``!`` is 

3540 also the first character of ``!=``. Excluding it would cut ``>=1!1.0`` short at the epoch. The operator 

3541 alternation is tried before the version at every position, so ``!=`` is still read as an operator wherever a 

3542 constraint can begin. 

3543 

3544 This is a function rather than a method, so :class:`VersionExpression` can call it in its own class body. A 

3545 dialect derived from it is served by :meth:`VersionExpression.__init_subclass__`. 

3546 

3547 :param operators: The dialect's operator spellings. 

3548 :param separators: The dialect's constraint separators. 

3549 :param versionType: The dialect's version class, which names the epoch separator to keep. 

3550 :returns: The pattern, with the operator as group 1 and the version as group 2. 

3551 """ 

3552 alternation = "|".join(re_escape(operator) for operator in sorted(operators, key=len, reverse=True)) 

3553 excluded = (set("".join(operators)) | set(separators)) - set(versionType._EPOCH_SEPARATOR) 

3554 

3555 # WORKAROUND: Python <3.12 

3556 # Reusing the f-string's own quote character inside its expression needs PEP 701, so the escaped character 

3557 # class is built into a variable first. On 3.11 the inlined form is a 'SyntaxError: f-string: unmatched (', 

3558 # raised at import, which takes the whole package with it. 

3559 # Replace by: 

3560 # return re_compile(rf"({alternation})?\s*([^\s{re_escape("".join(sorted(excluded)))}]+)") 

3561 excludedCharacters = re_escape("".join(sorted(excluded))) 

3562 

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

3564 

3565 

3566@export 

3567class VersionExpression(Generic[V], metaclass=ExtendedType, slots=True): 

3568 """ 

3569 A conjunction of :class:`VersionConstraint`\\ s. 

3570 

3571 An expression is written e.g. as ``>=1.2.0,<2.0.0``. Every constraint has to be satisfied, which is what separating 

3572 them means in every packaging ecosystem that has the notion. An expression with **no** constraints matches every 

3573 version, so *no version restriction* can be represented rather than special-cased by its callers. 

3574 

3575 .. code-block:: python 

3576 

3577 expression = VersionExpression.Parse(">=1.2.0,<2.0.0") 

3578 SemanticVersion.Parse("1.5.0") in expression # True 

3579 SemanticVersion.Parse("2.0.0") in expression # False 

3580 

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

3582 

3583 This class is the **ecosystem-neutral** dialect: the six ordering comparisons in their canonical spelling, 

3584 separated by commas or whitespace. An ecosystem that spells its operators differently, or adds a shorthand, 

3585 derives from it and overrides :attr:`_OPERATORS`, :attr:`_SEPARATORS`, :attr:`_VERSION_TYPE` or 

3586 :attr:`_SHORTHANDS`. A dialect is data, not behavior. 

3587 

3588 .. seealso:: 

3589 

3590 :class:`PythonVersionExpression` 

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

3592 """ 

3593 

3594 #: Operator spellings this dialect accepts, mapped onto the comparison they mean. 

3595 _OPERATORS: ClassVar[dict[str, VersionComparison]] = { 

3596 "==": VersionComparison.Equal, 

3597 "!=": VersionComparison.Unequal, 

3598 "<=": VersionComparison.LessThanOrEqual, 

3599 ">=": VersionComparison.GreaterThanOrEqual, 

3600 "<": VersionComparison.LessThan, 

3601 ">": VersionComparison.GreaterThan, 

3602 } 

3603 

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

3605 _SEPARATORS: ClassVar[str] = "," 

3606 

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

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

3609 

3610 #: The class each shorthand comparison is built as. A comparison absent here is a plain 

3611 #: :class:`VersionConstraint`; the neutral dialect has no shorthand at all. 

3612 _SHORTHANDS: ClassVar[dict[VersionComparison, type]] = {} 

3613 

3614 #: This dialect's compiled constraint pattern, built from the three tables above. A derived dialect gets its own 

3615 #: in :meth:`__init_subclass__`. 

3616 _CONSTRAINT_PATTERN: ClassVar[Pattern[str]] = _BuildConstraintPattern(_OPERATORS, _SEPARATORS, _VERSION_TYPE) 

3617 

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

3619 

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

3621 """ 

3622 Initialize an expression from its constraints. 

3623 

3624 :param constraints: Optional, the constraints a version has to satisfy. None of them means *any version*. 

3625 :raises ValueError: If parameter 'constraints' is None. 

3626 :raises TypeError: If parameter 'constraints' is not iterable. 

3627 :raises TypeError: If parameter 'constraints' contains an item that is not a :class:`VersionConstraint`. 

3628 """ 

3629 if constraints is None: 

3630 raise ValueError("Parameter 'constraints' is None.") 

3631 elif not isinstance(constraints, abc_Iterable): 

3632 ex = TypeError("Parameter 'constraints' is not iterable.") 

3633 ex.add_note(f"Got type '{getFullyQualifiedName(constraints)}'.") 

3634 raise ex 

3635 

3636 items = tuple(constraints) 

3637 for constraint in items: 

3638 if not isinstance(constraint, VersionConstraint): 3638 ↛ 3639line 3638 didn't jump to line 3639 because the condition on line 3638 was never true

3639 ex = TypeError("Parameter 'constraints' contains an item that is not of type 'VersionConstraint'.") 

3640 ex.add_note(f"Got type '{getFullyQualifiedName(constraint)}'.") 

3641 raise ex 

3642 

3643 self._constraints = items 

3644 

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

3646 """ 

3647 Compile the constraint pattern of a newly defined dialect. 

3648 

3649 A dialect is data: its operators, its separators and its version type are class variables, so its pattern is 

3650 settled once the class body has been read and is built here instead of on every :meth:`Parse`. 

3651 

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

3653 """ 

3654 super().__init_subclass__(**kwargs) 

3655 

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

3657 

3658 @classmethod 

3659 def Parse(cls, expression: Nullable[str], versionType: Nullable[type[Version]] = None) -> Self: 

3660 """ 

3661 Parse an expression into its constraints. 

3662 

3663 An expression is written e.g. as ``>=1.2.0,<2.0.0``. A constraint without an operator is an equality, so ``1.2.0`` 

3664 and ``==1.2.0`` are the same statement. An empty expression yields an expression with no constraints, which every 

3665 version satisfies - that is how *no version restriction* is written. 

3666 

3667 The expression is *scanned* rather than split, so a dialect separating constraints by whitespace does not 

3668 break a constraint that has whitespace after its operator. 

3669 

3670 :param expression: The expression to parse, or ``None`` for *any version*. 

3671 :param versionType: Optional, the :class:`Version` class the versions are parsed as. Defaults to the 

3672 dialect's :attr:`_VERSION_TYPE`. 

3673 :returns: The parsed expression. 

3674 :raises TypeError: If parameter 'expression' is not a string. 

3675 :raises ValueError: If the expression holds input this dialect doesn't accept. |br| 

3676 The note names the operators this dialect accepts. 

3677 :raises ValueError: If a version in the expression can't be parsed. |br| 

3678 The note names the operators this dialect accepts, because an operator another 

3679 ecosystem spells differently is read as part of the version. 

3680 """ 

3681 if expression is None: 

3682 return cls() 

3683 elif not isinstance(expression, str): 

3684 ex = TypeError("Parameter 'expression' is not of type 'str'.") 

3685 ex.add_note(f"Got type '{getFullyQualifiedName(expression)}'.") 

3686 raise ex 

3687 elif (expression := expression.strip()) == "": 

3688 return cls() 

3689 

3690 versionType = cls._VERSION_TYPE if versionType is None else versionType 

3691 skippable = cls._SEPARATORS + " \t" 

3692 # 'V' is unbound here - the version class comes from the dialect or the parameter, not from the type variable. 

3693 constraints: list[VersionConstraint[Any]] = [] 

3694 position = 0 

3695 

3696 for match in cls._CONSTRAINT_PATTERN.finditer(expression): 

3697 if (skipped := expression[position:match.start()].strip(skippable)) != "": 

3698 ex = ValueError(f"Expression '{expression}' has unexpected input at '{skipped}'.") 

3699 ex.add_note(f"This dialect accepts the operators {', '.join(sorted(cls._OPERATORS))}.") 

3700 raise ex 

3701 

3702 operator = match.group(1) 

3703 comparison = VersionComparison.Equal if operator is None else cls._OPERATORS[operator] 

3704 try: 

3705 version = versionType.Parse(match.group(2)) 

3706 except ValueError as cause: 

3707 # An operator this dialect doesn't know is not recognized as one, so it lands in the version instead. 

3708 ex = ValueError(f"Expression '{expression}' has unexpected input at '{match.group(0).strip()}'.") 

3709 ex.add_note(f"This dialect accepts the operators {', '.join(sorted(cls._OPERATORS))}.") 

3710 raise ex from cause 

3711 

3712 if (shorthand := cls._SHORTHANDS.get(comparison, None)) is not None: 

3713 constraints.append(shorthand(version)) 

3714 else: 

3715 constraints.append(VersionConstraint(comparison, version)) 

3716 position = match.end() 

3717 

3718 if (skipped := expression[position:].strip(skippable)) != "": 3718 ↛ 3719line 3718 didn't jump to line 3719 because the condition on line 3718 was never true

3719 ex = ValueError(f"Expression '{expression}' has unexpected input at '{skipped}'.") 

3720 ex.add_note(f"This dialect accepts the operators {', '.join(sorted(cls._OPERATORS))}.") 

3721 raise ex 

3722 

3723 return cls(constraints) 

3724 

3725 @readonly 

3726 def Constraints(self) -> tuple[VersionConstraint[V], ...]: 

3727 """ 

3728 Read-only property to access the constraints a version has to satisfy (:attr:`_constraints`). 

3729 

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

3731 """ 

3732 return self._constraints 

3733 

3734 @readonly 

3735 def MatchesAnyVersion(self) -> bool: 

3736 """ 

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

3738 

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

3740 """ 

3741 return len(self._constraints) == 0 

3742 

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

3744 """ 

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

3746 

3747 :param version: The version to check. 

3748 :returns: ``True``, if the version satisfies all constraints. An expression without constraints is 

3749 satisfied by every version. 

3750 :raises TypeError: If parameter 'version' is not of type :class:`Version`. 

3751 """ 

3752 if not isinstance(version, Version): 

3753 ex = TypeError("Parameter 'version' is not of type 'Version'.") 

3754 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

3755 raise ex 

3756 

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

3758 

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

3760 """ 

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

3762 

3763 An expression is a conjunction, so its range is the **intersection** of its constraints' ranges, which 

3764 :meth:`VersionRange.__and__` computes. An expression with no constraints is the range unbound at both ends, 

3765 since both match every version. 

3766 

3767 Not every expression has a range. One containing an :attr:`~VersionComparison.Unequal` does not, because the 

3768 complement of a version is a union of two intervals - ``>=1.0,!=1.3,<2.0`` is an ordinary requirement with 

3769 no single range. That is why :class:`VersionExpression` is not replaced by :class:`VersionRange`: a range is 

3770 one interval, an expression is any conjunction, and the second is strictly more expressive. 

3771 

3772 :returns: The range of versions satisfying every constraint. 

3773 :raises ValueError: If a constraint describes no range, which an :attr:`~VersionComparison.Unequal` never 

3774 does. 

3775 :raises ValueError: If the constraints have no version in common. 

3776 """ 

3777 versionRange: VersionRange[_VersionType] = VersionRange(None, None) 

3778 for constraint in self._constraints: 

3779 versionRange = versionRange & constraint.ToVersionRange() 

3780 

3781 return versionRange 

3782 

3783 def __len__(self) -> int: 

3784 """ 

3785 Return the number of constraints in this expression. 

3786 

3787 :returns: Number of constraints. 

3788 """ 

3789 return len(self._constraints) 

3790 

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

3792 """ 

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

3794 

3795 :returns: An iterator over the constraints. 

3796 """ 

3797 return iter(self._constraints) 

3798 

3799 def __str__(self) -> str: 

3800 """ 

3801 Return the expression in this dialect's spelling. 

3802 

3803 A :class:`VersionConstraint` renders itself canonically, which is not what every dialect writes - Debian 

3804 spells :attr:`~VersionComparison.Equal` ``=`` and :attr:`~VersionComparison.LessThan` ``<<``. The dialect's 

3805 own operator table answers what it writes; where a dialect has several spellings for one comparison, the 

3806 first one wins. 

3807 

3808 :returns: The constraints, joined by this dialect's separator, or an empty string if it constrains nothing. 

3809 """ 

3810 separator: str = self._SEPARATORS[0] if len(self._SEPARATORS) > 0 else " " 

3811 spelled: list[str] = [] 

3812 

3813 for constraint in self._constraints: 

3814 for spelling, comparison in self._OPERATORS.items(): 3814 ↛ 3819line 3814 didn't jump to line 3819 because the loop on line 3814 didn't complete

3815 if comparison is constraint.Comparison: 

3816 spelled.append(f"{spelling}{constraint.Version}") 

3817 break 

3818 else: 

3819 spelled.append(str(constraint)) 

3820 

3821 return separator.join(spelled) 

3822 

3823 

3824@export 

3825class PythonVersionExpression(VersionExpression[V]): 

3826 """ 

3827 A version expression in the dialect :pep:`440` defines, which is what a Python requirement file writes. 

3828 

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

3830 :class:`PythonVersion`: 

3831 

3832 .. code-block:: python 

3833 

3834 expression = PythonVersionExpression.Parse("~=1.2.3") 

3835 PythonVersion.Parse("1.2.9") in expression # True 

3836 PythonVersion.Parse("1.3.0") in expression # False 

3837 

3838 .. seealso:: 

3839 

3840 :class:`CompatibleVersionConstraint` 

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

3842 """ 

3843 

3844 #: The neutral dialect's operators, plus the compatible release operator :pep:`440` defines. 

3845 _OPERATORS: ClassVar[dict[str, VersionComparison]] = { 

3846 **VersionExpression._OPERATORS, 

3847 "~=": VersionComparison.CompatibleRelease, 

3848 } 

3849 

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

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

3852 

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

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

3855 VersionComparison.CompatibleRelease: CompatibleVersionConstraint, 

3856 } 

3857 

3858 

3859@export 

3860class NPMVersionExpression(VersionExpression[V]): 

3861 """ 

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

3863 

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

3865 

3866 * constraints are separated by **whitespace**, and a comma is a syntax error; 

3867 * equality is written ``=``, never ``==``; 

3868 * there is **no** ``!=`` - npm cannot exclude a single version this way. 

3869 

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

3871 

3872 .. code-block:: python 

3873 

3874 expression = NPMVersionExpression.Parse("^1.2.3") 

3875 SemanticVersion.Parse("1.9.0") in expression # True 

3876 SemanticVersion.Parse("2.0.0") in expression # False 

3877 

3878 .. note:: 

3879 

3880 The shorthands ``1.2.x``, ``*``, the hyphen range ``1.2.3 - 2.3.4`` and the alternative ``||`` are **not** 

3881 parsed. The first three are further rewriting rules; ``||`` is a disjunction, which this class cannot hold 

3882 because every constraint of an expression has to be satisfied. 

3883 

3884 .. seealso:: 

3885 

3886 :class:`CaretVersionConstraint` |br| 

3887 :class:`TildeVersionConstraint` 

3888 """ 

3889 

3890 #: npm's operators. No ``==`` and no ``!=``; ``^`` and ``~`` are npm's own shorthands. 

3891 _OPERATORS: ClassVar[dict[str, VersionComparison]] = { 

3892 "<=": VersionComparison.LessThanOrEqual, 

3893 ">=": VersionComparison.GreaterThanOrEqual, 

3894 "<": VersionComparison.LessThan, 

3895 ">": VersionComparison.GreaterThan, 

3896 "=": VersionComparison.Equal, 

3897 "^": VersionComparison.Caret, 

3898 "~": VersionComparison.Tilde, 

3899 } 

3900 

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

3902 _SEPARATORS: ClassVar[str] = "" 

3903 

3904 #: npm is strict semantic versioning. 

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

3906 

3907 #: ``^`` and ``~`` each derive an upper bound, by different rules. 

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

3909 VersionComparison.Caret: CaretVersionConstraint, 

3910 VersionComparison.Tilde: TildeVersionConstraint, 

3911 } 

3912 

3913 

3914@export 

3915class DebianVersionExpression(VersionExpression[V]): 

3916 """ 

3917 A version expression in Debian's dialect, as a ``debian/control`` dependency writes it inside its parentheses. 

3918 

3919 Debian spells the strict comparisons ``<<`` and ``>>``, equality ``=``, and has **no** ``!=``. The obsolete 

3920 spellings ``<`` and ``>`` are deliberately **not** accepted: ``dpkg`` still takes them but warns, because they 

3921 historically meant ``<=`` and ``>=`` - reading them silently as the strict operators would invert their meaning. 

3922 

3923 .. code-block:: python 

3924 

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

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

3927 

3928 .. note:: 

3929 

3930 A Debian dependency states **one** constraint per package mention - ``pkg (>= 1.0), pkg (<< 2.0)`` - so an 

3931 expression here usually holds a single constraint. The comma is Debian's *dependency* separator, not a 

3932 constraint separator. 

3933 

3934 .. attention:: 

3935 

3936 Debian version strings are ``epoch:upstream-revision``. :class:`SemanticVersion` reads the revision as a 

3937 postfix, but an **epoch** (``2:1.2.3-1``) does not parse, and ``1.2.3-1`` renders back as ``1.2.3+1``. 

3938 Matching Debian versions faithfully needs a ``DebianVersion`` class, which pyTooling does not have. 

3939 """ 

3940 

3941 #: Debian's operators. ``<<`` and ``>>`` are the strict ones; there is no ``!=``. 

3942 _OPERATORS: ClassVar[dict[str, VersionComparison]] = { 

3943 "<<": VersionComparison.LessThan, 

3944 ">>": VersionComparison.GreaterThan, 

3945 "<=": VersionComparison.LessThanOrEqual, 

3946 ">=": VersionComparison.GreaterThanOrEqual, 

3947 "=": VersionComparison.Equal, 

3948 } 

3949 

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

3951 _SEPARATORS: ClassVar[str] = "," 

3952 

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

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