Coverage for src/git_changelog/versioning.py: 97.44%

181 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-04 17:35 +0200

1"""Utilities to handle different versioning schemes such as SemVer and PEP 440.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any, Literal, Protocol 

6 

7import semver 

8from packaging import version as packaging_version 

9 

10SemVerStrategy = Literal[ 

11 "major", 

12 "minor", 

13 "patch", 

14 "release", 

15] 

16PEP440Strategy = Literal[ 

17 "epoch", 

18 "release", 

19 "major", 

20 "minor", 

21 "micro", 

22 "patch", 

23 "pre", 

24 "alpha", 

25 "beta", 

26 "candidate", 

27 "post", 

28 "dev", 

29 "major+alpha", 

30 "major+beta", 

31 "major+candidate", 

32 "major+dev", 

33 "major+alpha+dev", 

34 "major+beta+dev", 

35 "major+candidate+dev", 

36 "minor+alpha", 

37 "minor+beta", 

38 "minor+candidate", 

39 "minor+dev", 

40 "minor+alpha+dev", 

41 "minor+beta+dev", 

42 "minor+candidate+dev", 

43 "micro+alpha", 

44 "micro+beta", 

45 "micro+candidate", 

46 "micro+dev", 

47 "micro+alpha+dev", 

48 "micro+beta+dev", 

49 "micro+candidate+dev", 

50 "alpha+dev", 

51 "beta+dev", 

52 "candidate+dev", 

53] 

54 

55 

56_release_kind = {"a": "alpha", "b": "beta", "c": "candidate", "rc": "candidate", "p": "post"} 

57 

58 

59class ParsedVersion(Protocol): 

60 """Base class for versioning schemes.""" 

61 

62 def __lt__(self, other: object) -> bool: ... 62 ↛ exitline 62 didn't jump to line 62, because

63 def __le__(self, other: object) -> bool: ... 63 ↛ exitline 63 didn't jump to line 63, because

64 def __eq__(self, other: object) -> bool: ... 64 ↛ exitline 64 didn't jump to line 64, because

65 def __ge__(self, other: object) -> bool: ... 65 ↛ exitline 65 didn't jump to line 65, because

66 def __gt__(self, other: object) -> bool: ... 66 ↛ exitline 66 didn't jump to line 66, because

67 def __ne__(self, other: object) -> bool: ... 67 ↛ exitline 67 didn't jump to line 67, because

68 

69 

70class SemVerVersion(semver.Version, ParsedVersion): # type: ignore[misc] 

71 """SemVer version.""" 

72 

73 def bump_major(self) -> SemVerVersion: # noqa: D102 

74 return SemVerVersion(*super().bump_major()) # type: ignore[misc] 

75 

76 def bump_minor(self) -> SemVerVersion: # noqa: D102 

77 return SemVerVersion(*super().bump_minor()) # type: ignore[misc] 

78 

79 def bump_patch(self) -> SemVerVersion: # noqa: D102 

80 return SemVerVersion(*super().bump_patch()) # type: ignore[misc] 

81 

82 def bump_release(self) -> SemVerVersion: 

83 """Bump from a pre-release to the same, regular release. 

84 

85 Returns: 

86 The same version, without pre-release or build metadata. 

87 """ 

88 return SemVerVersion(self.major, self.minor, self.patch) 

89 

90 

91class PEP440Version(packaging_version.Version, ParsedVersion): # type: ignore[misc] 

92 """PEP 440 version.""" 

93 

94 @classmethod 

95 def from_parts( 

96 cls, 

97 epoch: int | None = None, 

98 release: tuple[int, ...] | None = None, 

99 pre: tuple[str, int] | None = None, 

100 post: int | None = None, 

101 dev: int | None = None, 

102 ) -> PEP440Version: 

103 """Build a version from its parts. 

104 

105 Parameters: 

106 epoch: Version's epoch number. 

107 release: Version's release numbers. 

108 pre: Version's prerelease kind and number. 

109 post: Version's post number. 

110 dev: Version's dev number. 

111 

112 Returns: 

113 A PEP 440 version. 

114 """ 

115 # Since the original class only allows instantiating a version 

116 # by passing a string, we first create a dummy version "1" 

117 # and then re-assign its internal `_version` with the real one. 

118 version = cls("1") 

119 version._version = packaging_version._Version( 

120 epoch=epoch or 0, 

121 release=release or (), 

122 pre=pre, 

123 post=None if post is None else ("post", post), 

124 dev=None if dev is None else ("dev", dev), 

125 local=None, 

126 ) 

127 

128 # We also have to update its `_key` attribute. 

129 # This is a hack and I would prefer that such functionality 

130 # is exposed directly in the original class. 

131 version._key = packaging_version._cmpkey( 

132 version._version.epoch, 

133 version._version.release, 

134 version._version.pre, 

135 version._version.post, 

136 version._version.dev, 

137 version._version.local, 

138 ) 

139 

140 return version 

141 

142 def bump_epoch(self) -> PEP440Version: 

143 """Bump epoch. 

144 

145 Examples: 

146 >>> PEP440Version("1.0").bump_epoch() 

147 <Version('1!1.0')> 

148 >>> PEP440Version("0!1.0").bump_epoch() 

149 <Version('1!1.0')> 

150 >>> PEP440Version("1!1.0").bump_epoch() 

151 <Version('2!1.0')> 

152 >>> PEP440Version("1.0a2.post3").bump_epoch() 

153 <Version('2!1.0')> 

154 

155 Returns: 

156 Version with bumped epoch, same release, and the right parts reset to 0 or nothing. 

157 """ 

158 return PEP440Version.from_parts(epoch=self.epoch + 1, release=self.release) 

159 

160 def bump_release(self, level: int | None = None, *, trim: bool = False) -> PEP440Version: 

161 """Bump given release level. 

162 

163 Parameters: 

164 level: The release level to bump. 

165 

166 - 0 means major 

167 - 1 means minor 

168 - 2 means micro (patch) 

169 - 3+ don't have names 

170 - None means move from pre-release to release (unchanged) 

171 trim: Whether to trim all zeroes on the right after bumping. 

172 

173 Examples: 

174 >>> PEP440Version("1").bump_release(0) 

175 <Version('2')> 

176 >>> PEP440Version("1.1").bump_release(0) 

177 <Version('2.0')> 

178 >>> PEP440Version("1.1.1").bump_release(0) 

179 <Version('2.0.0')> 

180 >>> PEP440Version("1.1.1").bump_release(0, trim=True) 

181 <Version('2')> 

182 >>> PEP440Version("1a2.post3").bump_release(0) 

183 <Version('2')> 

184 

185 >>> PEP440Version("1").bump_release(1) 

186 <Version('1.1')> 

187 >>> PEP440Version("1.1").bump_release(1) 

188 <Version('1.2')> 

189 >>> PEP440Version("1.1.1").bump_release(1) 

190 <Version('1.2.0')> 

191 >>> PEP440Version("1.1.1").bump_release(1, trim=True) 

192 <Version('1.2')> 

193 >>> PEP440Version("1a2.post3").bump_release(1) 

194 <Version('1.1')> 

195 

196 >>> PEP440Version("1a0").bump_release() 

197 <Version('1')> 

198 >>> PEP440Version("1b1").bump_release() 

199 <Version('1')> 

200 >>> PEP440Version("1rc2").bump_release() 

201 <Version('1')> 

202 >>> PEP440Version("1a2.dev0").bump_release() 

203 <Version('1')> 

204 >>> PEP440Version("1post0").bump_release() 

205 ValueError: Cannot bump from post-release to release 

206 

207 Returns: 

208 Version with same epoch, bumped release level, and the right parts reset to 0 or nothing. 

209 """ 

210 release = list(self.release) 

211 

212 # When level is not specified, user wants to bump the version 

213 # as a "release", going out of alpha/beta/candidate phase. 

214 # So we simply keep the release part as it is, optionally trimming it. 

215 if level is None: 

216 # However if this is a post-release, this is an error: 

217 # we can't bump from a post-release to the same, regular release. 

218 if self.post is not None: 

219 raise ValueError("Cannot bump from post-release to release") 

220 if trim: 

221 while release[-1] == 0: 

222 release.pop() 

223 

224 # When level is specified, we bump the specified level. 

225 # If the given level is higher that the number of release parts, 

226 # we insert the missing parts as 0. 

227 else: 

228 try: 

229 release[level] += 1 

230 except IndexError: 

231 while len(release) < level: 

232 release.append(0) 

233 release.append(1) 

234 if trim: 

235 release = release[: level + 1] 

236 else: 

237 for index in range(level + 1, len(release)): 

238 release[index] = 0 

239 

240 # We rebuild the version with same epoch, updated release, 

241 # and pre/post/dev parts dropped. 

242 return PEP440Version.from_parts(epoch=self.epoch, release=tuple(release)) 

243 

244 def bump_major(self, *, trim: bool = False) -> PEP440Version: 

245 """Bump major. 

246 

247 Parameters: 

248 trim: Whether to trim all zeroes on the right after bumping. 

249 

250 Examples: 

251 >>> PEP440Version("1").bump_major() 

252 <Version('2')> 

253 >>> PEP440Version("1.1").bump_major() 

254 <Version('2.0')> 

255 >>> PEP440Version("1.1.1").bump_major() 

256 <Version('2.0.0')> 

257 >>> PEP440Version("1.1.1").bump_major(trim=True) 

258 <Version('2')> 

259 >>> PEP440Version("1a2.post3").bump_major() 

260 <Version('2')> 

261 

262 Returns: 

263 Version with same epoch, bumped major and the right parts reset to 0 or nothing. 

264 """ 

265 return self.bump_release(level=0, trim=trim) 

266 

267 def bump_minor(self, *, trim: bool = False) -> PEP440Version: 

268 """Bump minor. 

269 

270 Parameters: 

271 trim: Whether to trim all zeroes on the right after bumping. 

272 

273 Examples: 

274 >>> PEP440Version("1").bump_minor() 

275 <Version('1.1')> 

276 >>> PEP440Version("1.1").bump_minor() 

277 <Version('1.2')> 

278 >>> PEP440Version("1.1.1").bump_minor() 

279 <Version('1.2.0')> 

280 >>> PEP440Version("1.1.1").bump_minor(trim=True) 

281 <Version('1.2')> 

282 >>> PEP440Version("1a2.post3").bump_minor() 

283 <Version('1.1')> 

284 

285 Returns: 

286 Version with same epoch, same major, bumped minor and the right parts reset to 0 or nothing. 

287 """ 

288 return self.bump_release(level=1, trim=trim) 

289 

290 def bump_micro(self, *, trim: bool = False) -> PEP440Version: 

291 """Bump micro. 

292 

293 Parameters: 

294 trim: Whether to trim all zeroes on the right after bumping. 

295 

296 Examples: 

297 >>> PEP440Version("1").bump_micro() 

298 <Version('1.0.1')> 

299 >>> PEP440Version("1.1").bump_micro() 

300 <Version('1.1.1')> 

301 >>> PEP440Version("1.1.1").bump_micro() 

302 <Version('1.1.2')> 

303 >>> PEP440Version("1.1.1").bump_micro() 

304 <Version('1.1.2')> 

305 >>> PEP440Version("1.1.1.1").bump_micro() 

306 <Version('1.1.2.0')> 

307 >>> PEP440Version("1.1.1.1").bump_micro(trim=True) 

308 <Version('1.1.2')> 

309 >>> PEP440Version("1a2.post3").bump_micro() 

310 <Version('1.0.1')> 

311 

312 Returns: 

313 Version with same epoch, same major, same minor, bumped micro and the right parts reset to 0 or nothing. 

314 """ 

315 return self.bump_release(level=2, trim=trim) 

316 

317 def bump_pre(self, pre: Literal["a", "b", "c", "rc"] | None = None) -> PEP440Version: 

318 """Bump pre-release. 

319 

320 Parameters: 

321 pre: Kind of pre-release to bump. 

322 

323 - a means alpha 

324 - b means beta 

325 - c or rc means (release) candidate 

326 

327 Examples: 

328 >>> PEP440Version("1").bump_pre() 

329 ValueError: Cannot bump from release to alpha pre-release (use `dent_pre`) 

330 >>> PEP440Version("1a0").bump_pre() 

331 <Version('1a1')> 

332 >>> PEP440Version("1a0").bump_pre("a") 

333 <Version('1a1')> 

334 >>> PEP440Version("1a0.post0").bump_pre() 

335 <Version('1a1')> 

336 >>> PEP440Version("1b2").bump_pre("a") 

337 ValueError: Cannot bump from beta to alpha pre-release (use `dent_alpha`) 

338 >>> PEP440Version("1c2").bump_pre("a") 

339 ValueError: Cannot bump from candidate to alpha pre-release (use `dent_alpha`) 

340 

341 >>> PEP440Version("1").bump_pre("b") 

342 ValueError: Cannot bump from release to beta pre-release (use `dent_beta`) 

343 >>> PEP440Version("1a2").bump_pre("b") 

344 <Version('1b0')> 

345 >>> PEP440Version("1b2").bump_pre("b") 

346 <Version('1b3')> 

347 >>> PEP440Version("1c2").bump_pre("b") 

348 ValueError: Cannot bump from candidate to beta pre-release (use `dent_beta`) 

349 

350 >>> PEP440Version("1").bump_pre("c") 

351 ValueError: Cannot bump from release to candidate pre-release (use `dent_candidate`) 

352 >>> PEP440Version("1a2").bump_pre("c") 

353 <Version('1rc0')> 

354 >>> PEP440Version("1b2").bump_pre("rc") 

355 <Version('1rc0')> 

356 >>> PEP440Version("1rc2").bump_pre("c") 

357 <Version('1rc3')> 

358 

359 Returns: 

360 Version with same epoch, same release, bumped pre-release and the right parts reset to 0 or nothing. 

361 """ 

362 if self.pre is None: 

363 kind = _release_kind.get(pre, "") # type: ignore[arg-type] 

364 raise ValueError( 

365 f"Cannot bump from release to {kind + ' ' if kind else ''}pre-release (use `dent_{kind or 'pre'}`)", 

366 ) 

367 current_pre: Literal["a", "b", "c", "rc"] 

368 current_pre, number = self.pre # type: ignore[assignment] 

369 if pre is None: 

370 pre = current_pre 

371 if pre == current_pre: 

372 number += 1 

373 elif current_pre < pre: 

374 number = 0 

375 else: 

376 raise ValueError( 

377 f"Cannot bump from {_release_kind.get(current_pre, 'release')} to {_release_kind[pre]} pre-release (use `dent_{_release_kind[pre]}`)", 

378 ) 

379 return PEP440Version.from_parts(epoch=self.epoch, release=self.release, pre=(pre, number)) 

380 

381 def bump_alpha(self) -> PEP440Version: 

382 """Bump alpha-release. 

383 

384 Examples: 

385 >>> PEP440Version("1").bump_alpha() 

386 ValueError: Cannot bump from release to alpha pre-release (use `dent_alpha`) 

387 >>> PEP440Version("1a0").bump_alpha() 

388 <Version('1a1')> 

389 >>> PEP440Version("1a0").bump_alpha("a") 

390 <Version('1a1')> 

391 >>> PEP440Version("1a0.post0").bump_alpha() 

392 <Version('1a1')> 

393 

394 Returns: 

395 Version with same epoch, same release, bumped alpha pre-release and the right parts reset to 0 or nothing. 

396 """ 

397 return self.bump_pre("a") 

398 

399 def bump_beta(self) -> PEP440Version: 

400 """Bump beta-release. 

401 

402 Examples: 

403 >>> PEP440Version("1").bump_beta() 

404 ValueError: Cannot bump from release to beta pre-release (use `dent_beta`) 

405 >>> PEP440Version("1b0").bump_beta() 

406 <Version('1b1')> 

407 >>> PEP440Version("1b0").bump_beta() 

408 <Version('1b1')> 

409 >>> PEP440Version("1b0.post0").bump_beta() 

410 <Version('1b1')> 

411 

412 Returns: 

413 Version with same epoch, same release, bumped beta pre-release and the right parts reset to 0 or nothing. 

414 """ 

415 return self.bump_pre("b") 

416 

417 def bump_candidate(self) -> PEP440Version: 

418 """Bump candidate release. 

419 

420 Examples: 

421 >>> PEP440Version("1").bump_candidate() 

422 ValueError: Cannot bump from release to candidate pre-release (use `dent_candidate`) 

423 >>> PEP440Version("1c0").bump_candidate() 

424 <Version('1rc1')> 

425 >>> PEP440Version("1c0").bump_candidate() 

426 <Version('1rc1')> 

427 >>> PEP440Version("1c0.post0").bump_candidate() 

428 <Version('1rc1')> 

429 

430 Returns: 

431 Version with same epoch, same release, bumped candidate pre-release and the right parts reset to 0 or nothing. 

432 """ 

433 return self.bump_pre("rc") 

434 

435 def bump_post(self) -> PEP440Version: 

436 """Bump post-release. 

437 

438 Examples: 

439 >>> PEP440Version("1").bump_post() 

440 <Version('1.post0')> 

441 >>> PEP440Version("1.post0").bump_post() 

442 <Version('1.post1')> 

443 >>> PEP440Version("1a0.post0").bump_post() 

444 <Version('1a0.post1')> 

445 >>> PEP440Version("1.post0.dev1").bump_post() 

446 <Version('1.post1')> 

447 

448 Returns: 

449 Version with same epoch, same release, same pre-release, bumped post-release and the right parts reset to 0 or nothing. 

450 """ 

451 post = 0 if self.post is None else self.post + 1 

452 return PEP440Version.from_parts(epoch=self.epoch, release=self.release, pre=self.pre, post=post) 

453 

454 def bump_dev(self) -> PEP440Version: 

455 """Bump dev-release. 

456 

457 Examples: 

458 >>> PEP440Version("1").bump_dev() 

459 ValueError: Cannot bump from release to dev-release (use `dent_dev`) 

460 >>> PEP440Version("1a0").bump_dev() 

461 ValueError: Cannot bump from alpha to dev-release (use `dent_dev`) 

462 >>> PEP440Version("1b1").bump_dev() 

463 ValueError: Cannot bump from beta to dev-release (use `dent_dev`) 

464 >>> PEP440Version("1rc2").bump_dev() 

465 ValueError: Cannot bump from candidate to dev-release (use `dent_dev`) 

466 >>> PEP440Version("1.post0").bump_dev() 

467 ValueError: Cannot bump from post to dev-release (use `dent_dev`) 

468 >>> PEP440Version("1a0.dev1").bump_dev() 

469 <Version('1a0.dev2')> 

470 

471 Returns: 

472 Version with same epoch, same release, same pre-release, same post-release and bumped dev-release. 

473 """ 

474 if self.dev is None: 

475 if self.post is not None: 

476 kind = "p" 

477 elif self.pre is not None: 

478 kind = self.pre[0] 

479 else: 

480 kind = "z" 

481 raise ValueError(f"Cannot bump from {_release_kind.get(kind, 'release')} to dev-release (use `dent_dev`)") 

482 return PEP440Version.from_parts( 

483 epoch=self.epoch, 

484 release=self.release, 

485 pre=self.pre, 

486 post=self.post, 

487 dev=self.dev + 1, 

488 ) 

489 

490 def dent_pre(self, pre: Literal["a", "b", "c", "rc"] | None = None) -> PEP440Version: 

491 """Dent to pre-release. 

492 

493 This method dents a release down to an alpha, beta or candidate pre-release. 

494 

495 Parameters: 

496 pre: Kind of pre-release to bump. 

497 

498 - a means alpha 

499 - b means beta 

500 - c or rc means (release) candidate 

501 

502 Examples: 

503 >>> PEP440Version("1").dent_pre() 

504 <Version('1a0')> 

505 >>> PEP440Version("1").dent_pre("a") 

506 <Version('1a0')> 

507 >>> PEP440Version("1a0").dent_pre("a") 

508 ValueError: Cannot dent alpha pre-releases 

509 >>> PEP440Version("1").dent_pre("b") 

510 <Version('1b0')> 

511 >>> PEP440Version("1b0").dent_pre("b") 

512 ValueError: Cannot dent beta pre-releases 

513 >>> PEP440Version("1").dent_pre("c") 

514 <Version('1rc0')> 

515 >>> PEP440Version("1").dent_pre("rc") 

516 <Version('1rc0')> 

517 >>> PEP440Version("1rc0").dent_pre("c") 

518 ValueError: Cannot dent candidate pre-releases 

519 

520 Returns: 

521 Version with same epoch and release dented to pre-release. 

522 """ 

523 if self.pre is not None: 

524 raise ValueError(f"Cannot dent {_release_kind[self.pre[0]]} pre-releases") 

525 if pre is None: 

526 pre = "a" 

527 return PEP440Version.from_parts(epoch=self.epoch, release=self.release, pre=(pre, 0)) 

528 

529 def dent_alpha(self) -> PEP440Version: 

530 """Dent to alpha-release. 

531 

532 Examples: 

533 >>> PEP440Version("1").dent_alpha() 

534 <Version('1a0')> 

535 >>> PEP440Version("1a0").dent_alpha() 

536 ValueError: Cannot dent alpha pre-releases 

537 >>> PEP440Version("1b0").dent_alpha() 

538 ValueError: Cannot dent beta pre-releases 

539 >>> PEP440Version("1rc0").dent_alpha() 

540 ValueError: Cannot dent candidate pre-releases 

541 

542 Returns: 

543 Version with same epoch and release dented to alpha pre-release. 

544 """ 

545 return self.dent_pre("a") 

546 

547 def dent_beta(self) -> PEP440Version: 

548 """Dent to beta-release. 

549 

550 Examples: 

551 >>> PEP440Version("1").dent_beta() 

552 <Version('1b0')> 

553 >>> PEP440Version("1a0").dent_beta() 

554 ValueError: Cannot dent alpha pre-releases 

555 >>> PEP440Version("1b0").dent_beta() 

556 ValueError: Cannot dent beta pre-releases 

557 >>> PEP440Version("1rc0").dent_beta() 

558 ValueError: Cannot dent candidate pre-releases 

559 

560 Returns: 

561 Version with same epoch and release dented to beta pre-release. 

562 """ 

563 return self.dent_pre("b") 

564 

565 def dent_candidate(self) -> PEP440Version: 

566 """Dent to candidate release. 

567 

568 Examples: 

569 >>> PEP440Version("1").dent_candidate() 

570 <Version('1rc0')> 

571 >>> PEP440Version("1a0").dent_candidate() 

572 ValueError: Cannot dent alpha pre-releases 

573 >>> PEP440Version("1b0").dent_candidate() 

574 ValueError: Cannot dent beta pre-releases 

575 >>> PEP440Version("1rc0").dent_candidate() 

576 ValueError: Cannot dent candidate pre-releases 

577 

578 Returns: 

579 Version with same epoch and release dented to candidate pre-release. 

580 """ 

581 return self.dent_pre("rc") 

582 

583 def dent_dev(self) -> PEP440Version: 

584 """Dent to dev-release. 

585 

586 Examples: 

587 >>> PEP440Version("1").dent_dev() 

588 <Version('1.dev0')> 

589 >>> PEP440Version("1a0").dent_dev() 

590 <Version('1a0.dev0')> 

591 >>> PEP440Version("1b1").dent_dev() 

592 <Version('1b1.dev0')> 

593 >>> PEP440Version("1c2").dent_dev() 

594 <Version('1rc2.dev0')> 

595 >>> PEP440Version("1.post0").dent_dev() 

596 <Version('1.post0.dev0')> 

597 >>> PEP440Version("1a0.dev1").dent_dev() 

598 ValueError: Cannot dent dev-releases 

599 

600 Returns: 

601 Version with same epoch and release dented to dev-release. 

602 """ 

603 if self.dev is not None: 

604 raise ValueError("Cannot dent dev-releases") 

605 return PEP440Version.from_parts(epoch=self.epoch, release=self.release, pre=self.pre, post=self.post, dev=0) 

606 

607 

608def version_prefix(version: str) -> tuple[str, str]: 

609 """Return a version and its optional `v` prefix. 

610 

611 Arguments: 

612 version: The full version. 

613 

614 Returns: 

615 version: The version without its prefix. 

616 prefix: The version prefix. 

617 """ 

618 prefix = "" 

619 if version[0] == "v": 

620 prefix = "v" 

621 version = version[1:] 

622 return version, prefix 

623 

624 

625def parse_semver(version: str) -> tuple[SemVerVersion, str]: 

626 """Parse a SemVer version. 

627 

628 Returns: 

629 A semver version instance with useful methods. 

630 """ 

631 version, prefix = version_prefix(version) 

632 return SemVerVersion.parse(version), prefix 

633 

634 

635def parse_pep440(version: str) -> tuple[PEP440Version, str]: 

636 """Parse a PEP version. 

637 

638 Returns: 

639 A PEP 440 version instance with useful methods. 

640 """ 

641 version, prefix = version_prefix(version) 

642 return PEP440Version(version), prefix 

643 

644 

645class VersionBumper: 

646 """Base class for version bumpers.""" 

647 

648 initial: str 

649 

650 def __init__(self, strategies: tuple[str, ...]) -> None: 

651 """Initialize the bumper. 

652 

653 Parameters: 

654 strategies: The supported bumping strategies. 

655 """ 

656 self.strategies = strategies 

657 

658 def __call__(self, version: str, strategy: str = ..., **kwargs: Any) -> str: 

659 """Bump a version. 

660 

661 Parameters: 

662 version: The version to bump. 

663 strategy: The bumping strategy. 

664 **kwargs: Additional bumper-specific arguments. 

665 

666 Returns: 

667 The bumped version. 

668 """ 

669 raise NotImplementedError 

670 

671 

672class PEP440Bumper(VersionBumper): 

673 """PEP 440 version bumper.""" 

674 

675 initial: str = "0.0.0" 

676 

677 def __call__( # type: ignore[override] 

678 self, 

679 version: str, 

680 strategy: PEP440Strategy = "micro", 

681 *, 

682 zerover: bool = False, 

683 trim: bool = False, 

684 ) -> str: 

685 """Bump a PEP 440 version. 

686 

687 Arguments: 

688 version: The version to bump. 

689 strategy: The part of the version to bump. 

690 zerover: Keep major version at zero, even for breaking changes. 

691 trim: Whether to trim all zeroes on the right after bumping. 

692 

693 Returns: 

694 The bumped version. 

695 """ 

696 pep440_version, prefix = parse_pep440(version) 

697 

698 # Split into main part and pre/dev markers 

699 # (+alpha, +beta, +candidate, +dev). 

700 main_part, *predev = strategy.split("+") 

701 

702 # Bump main part. 

703 if main_part == "epoch": 

704 pep440_version = pep440_version.bump_epoch() 

705 elif main_part == "release": 

706 pep440_version = pep440_version.bump_release(trim=trim) 

707 elif main_part == "major": 

708 # If major version is 0 and zerover is active, only bump minor. 

709 if pep440_version.major == 0 and zerover: 

710 pep440_version = pep440_version.bump_minor(trim=trim) 

711 else: 

712 pep440_version = pep440_version.bump_major(trim=trim) 

713 elif main_part == "minor": 

714 pep440_version = pep440_version.bump_minor(trim=trim) 

715 elif main_part in ("micro", "patch"): 

716 pep440_version = pep440_version.bump_micro(trim=trim) 

717 elif main_part == "pre": 

718 pep440_version = pep440_version.bump_pre() 

719 elif main_part == "alpha": 

720 pep440_version = pep440_version.bump_alpha() 

721 elif main_part == "beta": 

722 pep440_version = pep440_version.bump_beta() 

723 elif main_part == "candidate": 

724 pep440_version = pep440_version.bump_candidate() 

725 elif main_part == "post": 

726 pep440_version = pep440_version.bump_post() 

727 elif main_part == "dev": 

728 pep440_version = pep440_version.bump_dev() 

729 else: 

730 raise ValueError(f"Invalid strategy {main_part}, use one of {', '.join(self.strategies)}") 

731 

732 # Dent to pre-release (+alpha, +beta, +candidate). 

733 if "alpha" in predev: 

734 pep440_version = pep440_version.dent_alpha() 

735 elif "beta" in predev: 

736 pep440_version = pep440_version.dent_beta() 

737 elif "candidate" in predev: 

738 pep440_version = pep440_version.dent_candidate() 

739 

740 # Dent to dev-release (+dev). 

741 if "dev" in predev: 

742 pep440_version = pep440_version.dent_dev() 

743 

744 # Return new version with preserved prefix. 

745 return prefix + str(pep440_version) 

746 

747 

748class SemVerBumper(VersionBumper): 

749 """SemVer version bumper.""" 

750 

751 initial: str = "0.0.0" 

752 

753 def __call__( # type: ignore[override] 

754 self, 

755 version: str, 

756 strategy: SemVerStrategy = "patch", 

757 *, 

758 zerover: bool = True, 

759 ) -> str: 

760 """Bump a SemVer version. 

761 

762 Arguments: 

763 version: The version to bump. 

764 part: The part of the version to bump. 

765 zerover: Keep major version at zero, even for breaking changes. 

766 

767 Returns: 

768 The bumped version. 

769 """ 

770 semver_version, prefix = parse_semver(version) 

771 if strategy == "major": 

772 if semver_version.major == 0 and zerover: 

773 semver_version = semver_version.bump_minor() 

774 else: 

775 semver_version = semver_version.bump_major() 

776 elif strategy == "minor": 

777 semver_version = semver_version.bump_minor() 

778 elif strategy == "patch": 

779 semver_version = semver_version.bump_patch() 

780 elif strategy == "release": 

781 semver_version = semver_version.bump_release() 

782 else: 

783 raise ValueError(f"Invalid strategy {strategy}, use one of {', '.join(self.strategies)}") 

784 return prefix + str(semver_version) 

785 

786 

787bump_pep440 = PEP440Bumper(PEP440Strategy.__args__) # type: ignore[attr-defined] 

788"""Bump a PEP 440 version.""" 

789bump_semver = SemVerBumper(SemVerStrategy.__args__) # type: ignore[attr-defined] 

790"""Bump a SemVer version."""