Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/packaging/_ranges.py: 30%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

379 statements  

1# This file is dual licensed under the terms of the Apache License, Version 

2# 2.0, and the BSD License. See the LICENSE file in the root of this repository 

3# for complete details. 

4"""Private version-range helpers used by :mod:`packaging.specifiers`.""" 

5 

6from __future__ import annotations 

7 

8import enum 

9import functools 

10from typing import ( 

11 TYPE_CHECKING, 

12 Any, 

13 Final, 

14) 

15 

16from .version import InvalidVersion, Version 

17 

18if TYPE_CHECKING: 

19 from collections.abc import Callable, Iterable, Iterator, Sequence 

20 from typing import Union 

21 

22__all__ = [ 

23 "FULL_RANGE", 

24 "filter_by_ranges", 

25 "intersect_ranges", 

26 "ranges_are_prerelease_only", 

27 "standard_ranges", 

28 "wildcard_ranges", 

29] 

30 

31#: The smallest possible PEP 440 version. No valid version is less than this. 

32_MIN_VERSION: Final[Version] = Version("0.dev0") 

33 

34 

35class _BoundaryKind(enum.Enum): 

36 """Where a boundary marker sits in the version ordering.""" 

37 

38 AFTER_LOCALS = enum.auto() # after V+local, before V.post0 

39 AFTER_POSTS = enum.auto() # after V.postN, before next release 

40 

41 

42@functools.total_ordering 

43class _BoundaryVersion: 

44 """A point on the version line between two real PEP 440 versions. 

45 

46 Relative to a base version V:: 

47 

48 V < V+local < AFTER_LOCALS(V) < V.post0 < AFTER_POSTS(V) 

49 

50 AFTER_LOCALS is the upper bound of ``<=V``, ``==V``, ``!=V`` (no 

51 local), and the lower bound of the upper-side range of ``!=V``. 

52 AFTER_POSTS is the lower bound of ``>V`` (V final or pre-release), 

53 excluding V's post-releases per PEP 440. 

54 """ 

55 

56 __slots__ = ( 

57 "_cached_dev", 

58 "_cached_epoch", 

59 "_cached_post", 

60 "_cached_pre", 

61 "_cached_trimmed_release", 

62 "_kind", 

63 "version", 

64 ) 

65 

66 def __init__(self, version: Version, kind: _BoundaryKind) -> None: 

67 self.version = version 

68 self._kind = kind 

69 self._cached_trimmed_release = trim_release(version.release) 

70 self._cached_epoch = version.epoch 

71 self._cached_pre = version.pre 

72 self._cached_post = version.post 

73 self._cached_dev = version.dev 

74 

75 def _is_family(self, other: Version) -> bool: 

76 """Is ``other`` a version that this boundary sorts above?""" 

77 if other.epoch != self._cached_epoch: 

78 return False 

79 # Inline release-trim comparison: other.release matches the 

80 # trimmed release iff its leading slice is equal and any extra 

81 # components are zero. Avoids trim_release's tuple allocation. 

82 other_release = other.release 

83 trimmed_release = self._cached_trimmed_release 

84 trimmed_length = len(trimmed_release) 

85 if len(other_release) < trimmed_length: 

86 return False 

87 if other_release[:trimmed_length] != trimmed_release: 

88 return False 

89 for i in range(trimmed_length, len(other_release)): 

90 if other_release[i] != 0: 

91 return False 

92 if other.pre != self._cached_pre: 

93 return False 

94 if self._kind == _BoundaryKind.AFTER_LOCALS: 

95 # Local family: same public version, any local label. 

96 return other.post == self._cached_post and other.dev == self._cached_dev 

97 # Post family: V itself + any post-release of V. 

98 return other.dev == self._cached_dev or other.post is not None 

99 

100 def __eq__(self, other: object) -> bool: 

101 if isinstance(other, _BoundaryVersion): 

102 return self.version == other.version and self._kind == other._kind 

103 return NotImplemented 

104 

105 def __lt__(self, other: _BoundaryVersion | Version) -> bool: 

106 if isinstance(other, _BoundaryVersion): 

107 if self.version != other.version: 

108 return self.version < other.version 

109 return self._kind.value < other._kind.value # pragma: no cover 

110 # boundary < other_version iff V < other AND other not in family. 

111 # The cheap V >= other path short-circuits before the family check. 

112 if not (self.version < other): 

113 return False 

114 return not self._is_family(other) 

115 

116 def __gt__(self, other: _BoundaryVersion | Version) -> bool: 

117 # Defined directly to bypass functools.total_ordering's 

118 # NotImplemented round-trip on reflected ``Version < boundary``. 

119 if isinstance(other, _BoundaryVersion): 

120 if self.version != other.version: 

121 return self.version > other.version 

122 return self._kind.value > other._kind.value 

123 if self.version >= other: 

124 return True 

125 return self._is_family(other) 

126 

127 def __hash__(self) -> int: 

128 return hash((self.version, self._kind)) 

129 

130 def __repr__(self) -> str: 

131 return f"{self.__class__.__name__}({self.version!r}, {self._kind.name})" 

132 

133 

134if TYPE_CHECKING: 

135 _VersionOrBoundary = Union[Version, _BoundaryVersion, None] 

136 

137 

138@functools.total_ordering 

139class _LowerBound: 

140 """Lower bound of a version range. 

141 

142 A version *v* of ``None`` means unbounded below (-inf). 

143 At equal versions, ``[v`` sorts before ``(v`` because an inclusive 

144 bound starts earlier. 

145 """ 

146 

147 __slots__ = ("_above", "inclusive", "version") 

148 

149 def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None: 

150 self.version = version 

151 self.inclusive = inclusive 

152 # Pre-bind a predicate "is parsed at or above this lower 

153 # bound?" for the hot filter / contains loops. One direct 

154 # call per check, no operator-dispatch chain. 

155 if version is None: 

156 self._above: Callable[[Version], bool] | None = None 

157 elif isinstance(version, _BoundaryVersion): 

158 # >V produces an AFTER_POSTS lower bound; the upper-side 

159 # range of !=V produces an AFTER_LOCALS lower bound. 

160 if version._kind == _BoundaryKind.AFTER_POSTS: 

161 self._above = _make_above_after_posts(version.version) 

162 else: 

163 self._above = _make_above_after_locals(version.version) 

164 elif inclusive: 

165 self._above = version.__le__ 

166 else: 

167 self._above = version.__lt__ 

168 

169 def __eq__(self, other: object) -> bool: 

170 if not isinstance(other, _LowerBound): 

171 return NotImplemented # pragma: no cover 

172 return self.version == other.version and self.inclusive == other.inclusive 

173 

174 def __lt__(self, other: _LowerBound) -> bool: 

175 if not isinstance(other, _LowerBound): # pragma: no cover 

176 return NotImplemented 

177 # -inf < anything (except -inf itself). 

178 if self.version is None: 

179 return other.version is not None 

180 if other.version is None: 

181 return False 

182 if self.version != other.version: 

183 return self.version < other.version 

184 # [v < (v: inclusive starts earlier. 

185 return self.inclusive and not other.inclusive 

186 

187 def __hash__(self) -> int: 

188 return hash((self.version, self.inclusive)) 

189 

190 def __repr__(self) -> str: 

191 bracket = "[" if self.inclusive else "(" 

192 return f"<{self.__class__.__name__} {bracket}{self.version!r}>" 

193 

194 

195@functools.total_ordering 

196class _UpperBound: 

197 """Upper bound of a version range. 

198 

199 A version *v* of ``None`` means unbounded above (+inf). 

200 At equal versions, ``v)`` sorts before ``v]`` because an exclusive 

201 bound ends earlier. 

202 """ 

203 

204 __slots__ = ("_below", "inclusive", "version") 

205 

206 def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None: 

207 self.version = version 

208 self.inclusive = inclusive 

209 # Pre-bind a predicate "is parsed at or below this upper 

210 # bound?". See _LowerBound for the rationale. 

211 if version is None: 

212 self._below: Callable[[Version], bool] | None = None 

213 elif isinstance(version, _BoundaryVersion): 

214 # Standard specifiers only ever produce AFTER_LOCALS upper 

215 # bounds (from <=V / ==V / !=V with no local). 

216 if version._kind == _BoundaryKind.AFTER_LOCALS: 

217 self._below = _make_below_after_locals(version.version) 

218 else: # pragma: no cover (AFTER_POSTS upper not produced by specifiers) 

219 self._below = version.__ge__ 

220 elif inclusive: 

221 self._below = version.__ge__ 

222 else: 

223 self._below = version.__gt__ 

224 

225 def __eq__(self, other: object) -> bool: 

226 if not isinstance(other, _UpperBound): 

227 return NotImplemented # pragma: no cover 

228 return self.version == other.version and self.inclusive == other.inclusive 

229 

230 def __lt__(self, other: _UpperBound) -> bool: 

231 if not isinstance(other, _UpperBound): # pragma: no cover 

232 return NotImplemented 

233 # Nothing < +inf (except +inf itself). 

234 if self.version is None: 

235 return False 

236 if other.version is None: 

237 return True 

238 if self.version != other.version: 

239 return self.version < other.version 

240 # v) < v]: exclusive ends earlier. 

241 return not self.inclusive and other.inclusive 

242 

243 def __hash__(self) -> int: 

244 return hash((self.version, self.inclusive)) 

245 

246 def __repr__(self) -> str: 

247 bracket = "]" if self.inclusive else ")" 

248 return f"<{self.__class__.__name__} {self.version!r}{bracket}>" 

249 

250 

251if TYPE_CHECKING: 

252 #: A single contiguous version range, as a (lower, upper) pair. 

253 VersionRange = tuple[_LowerBound, _UpperBound] 

254 

255 

256_NEG_INF: Final[_LowerBound] = _LowerBound(None, False) 

257_POS_INF: Final[_UpperBound] = _UpperBound(None, False) 

258FULL_RANGE: Final[tuple[VersionRange]] = ((_NEG_INF, _POS_INF),) 

259 

260 

261def trim_release(release: tuple[int, ...]) -> tuple[int, ...]: 

262 """Strip trailing zeros from a release tuple for normalized comparison.""" 

263 end = len(release) 

264 while end > 1 and release[end - 1] == 0: 

265 end -= 1 

266 return release if end == len(release) else release[:end] 

267 

268 

269def _next_prefix_dev0(version: Version) -> Version: 

270 """Smallest version in the next prefix: 1.2 -> 1.3.dev0.""" 

271 release = (*version.release[:-1], version.release[-1] + 1) 

272 return Version.from_parts(epoch=version.epoch, release=release, dev=0) 

273 

274 

275def _base_dev0(version: Version) -> Version: 

276 """The .dev0 of a version's base release: 1.2 -> 1.2.dev0.""" 

277 return Version.from_parts(epoch=version.epoch, release=version.release, dev=0) 

278 

279 

280def _coerce_version(version: Version | str) -> Version | None: 

281 if not isinstance(version, Version): 

282 try: 

283 version = Version(version) 

284 except InvalidVersion: 

285 return None 

286 return version 

287 

288 

289def _make_above_after_posts(version: Version) -> Callable[[Version], bool]: 

290 """Predicate ``parsed > AFTER_POSTS(V)`` for a lower bound. 

291 

292 Per PEP 440, ``>V`` excludes V's post-releases unless V is itself 

293 a post-release. AFTER_POSTS sits above V and every V.postN (with 

294 or without local), and just below the next release. 

295 """ 

296 version_ge = version.__ge__ 

297 version_epoch = version.epoch 

298 version_pre = version.pre 

299 version_dev = version.dev 

300 version_release_trimmed = trim_release(version.release) 

301 trimmed_length = len(version_release_trimmed) 

302 

303 def above(parsed: Version) -> bool: 

304 if version_ge(parsed): 

305 return False 

306 # parsed > V cmpkey-wise: above the boundary iff NOT in V's 

307 # post family. 

308 if parsed.epoch != version_epoch: 

309 return True 

310 parsed_release = parsed.release 

311 if len(parsed_release) < trimmed_length: 

312 return True 

313 if parsed_release[:trimmed_length] != version_release_trimmed: 

314 return True 

315 for i in range(trimmed_length, len(parsed_release)): 

316 if parsed_release[i] != 0: 

317 return True 

318 if parsed.pre != version_pre: 

319 return True 

320 # In post family iff: same dev as V (covers V itself + V+local), 

321 # or any post-release (covers V.postN + V.postN+local). 

322 if parsed.dev == version_dev or parsed.post is not None: 

323 return False 

324 # Different dev with no post means parsed sorts before V 

325 # cmpkey-wise, in which case version_ge returned True already. 

326 return False # pragma: no cover 

327 

328 return above 

329 

330 

331def _make_above_after_locals(version: Version) -> Callable[[Version], bool]: 

332 """Predicate ``parsed > AFTER_LOCALS(V)`` for a lower bound. 

333 

334 Used by the upper-side range of ``!=V`` (when V has no local 

335 segment). AFTER_LOCALS sits above V and every ``V+local`` but 

336 just below ``V.post0``. 

337 """ 

338 version_ge = version.__ge__ 

339 version_epoch = version.epoch 

340 version_pre = version.pre 

341 version_post = version.post 

342 version_dev = version.dev 

343 version_release_trimmed = trim_release(version.release) 

344 trimmed_length = len(version_release_trimmed) 

345 

346 def above(parsed: Version) -> bool: 

347 if version_ge(parsed): 

348 return False 

349 # parsed > V cmpkey-wise: above the boundary iff NOT in V's 

350 # local family (same public version, any local segment). 

351 if parsed.epoch != version_epoch: 

352 return True 

353 parsed_release = parsed.release 

354 if len(parsed_release) < trimmed_length: 

355 return True 

356 if parsed_release[:trimmed_length] != version_release_trimmed: 

357 return True 

358 for i in range(trimmed_length, len(parsed_release)): 

359 if parsed_release[i] != 0: 

360 return True 

361 if parsed.pre != version_pre: 

362 return True 

363 if parsed.post != version_post: 

364 return True 

365 return parsed.dev != version_dev 

366 

367 return above 

368 

369 

370def _make_below_after_locals(version: Version) -> Callable[[Version], bool]: 

371 """Predicate ``parsed <= AFTER_LOCALS(V)`` for an upper bound. 

372 

373 Used by ``<=V``, ``==V``, ``!=V`` (no local). ``parsed`` is at or 

374 below the boundary when it is at or below V cmpkey-wise, or when 

375 it is in V's local family. 

376 """ 

377 version_ge = version.__ge__ 

378 version_epoch = version.epoch 

379 version_pre = version.pre 

380 version_post = version.post 

381 version_dev = version.dev 

382 version_release_trimmed = trim_release(version.release) 

383 trimmed_length = len(version_release_trimmed) 

384 

385 def below(parsed: Version) -> bool: 

386 if version_ge(parsed): 

387 return True 

388 # parsed > V cmpkey-wise: below the boundary iff in V's local 

389 # family. 

390 if parsed.epoch != version_epoch: 

391 return False 

392 parsed_release = parsed.release 

393 if len(parsed_release) < trimmed_length: 

394 return False 

395 if parsed_release[:trimmed_length] != version_release_trimmed: 

396 return False 

397 for i in range(trimmed_length, len(parsed_release)): 

398 if parsed_release[i] != 0: 

399 return False 

400 if parsed.pre != version_pre: 

401 return False 

402 if parsed.post != version_post: 

403 return False 

404 return parsed.dev == version_dev 

405 

406 return below 

407 

408 

409def _range_is_empty(lower: _LowerBound, upper: _UpperBound) -> bool: 

410 """True when the range defined by *lower* and *upper* contains no versions.""" 

411 if lower.version is None or upper.version is None: 

412 return False 

413 if lower.version == upper.version: 

414 return not (lower.inclusive and upper.inclusive) 

415 return lower.version > upper.version 

416 

417 

418def intersect_ranges( 

419 left: Sequence[VersionRange], 

420 right: Sequence[VersionRange], 

421) -> list[VersionRange]: 

422 """Intersect two sorted, non-overlapping range lists (two-pointer merge).""" 

423 result: list[VersionRange] = [] 

424 left_index = right_index = 0 

425 while left_index < len(left) and right_index < len(right): 

426 left_lower, left_upper = left[left_index] 

427 right_lower, right_upper = right[right_index] 

428 

429 lower = max(left_lower, right_lower) 

430 upper = min(left_upper, right_upper) 

431 

432 if not _range_is_empty(lower, upper): 

433 result.append((lower, upper)) 

434 

435 # Advance whichever side has the smaller upper bound. 

436 if left_upper < right_upper: 

437 left_index += 1 

438 else: 

439 right_index += 1 

440 

441 return result 

442 

443 

444def filter_by_ranges( 

445 ranges: Sequence[VersionRange], 

446 iterable: Iterable[Any], 

447 key: Callable[[Any], Version | str] | None, 

448 prereleases: bool | None, 

449) -> Iterator[Any]: 

450 """Filter *iterable* against precomputed version *ranges*. 

451 

452 With ``prereleases=None``, the PEP 440 default applies: pre-releases 

453 are excluded unless no final matches, in which case buffered 

454 pre-releases come out at the end. 

455 """ 

456 if prereleases is None: 

457 # PEP 440 default: yield finals immediately; buffer 

458 # pre-releases until at least one final has been emitted. 

459 prerelease_buffer: list[Any] = [] 

460 found_final = False 

461 

462 if len(ranges) == 1: 

463 lower, upper = ranges[0] 

464 above = lower._above 

465 below = upper._below 

466 for item in iterable: 

467 parsed = _coerce_version(item if key is None else key(item)) 

468 if parsed is None: 

469 continue 

470 if above is not None and not above(parsed): 

471 continue 

472 if below is not None and not below(parsed): 

473 continue 

474 if parsed.is_prerelease: 

475 if not found_final: 

476 prerelease_buffer.append(item) 

477 else: 

478 found_final = True 

479 yield item 

480 if not found_final: 

481 yield from prerelease_buffer 

482 return 

483 

484 for item in iterable: 

485 parsed = _coerce_version(item if key is None else key(item)) 

486 if parsed is None: 

487 continue 

488 for lower, upper in ranges: 

489 above = lower._above 

490 if above is not None and not above(parsed): 

491 break 

492 below = upper._below 

493 if below is None or below(parsed): 

494 if parsed.is_prerelease: 

495 if not found_final: 

496 prerelease_buffer.append(item) 

497 else: 

498 found_final = True 

499 yield item 

500 break 

501 if not found_final: 

502 yield from prerelease_buffer 

503 return 

504 

505 exclude_prereleases = prereleases is False 

506 

507 if len(ranges) == 1: 

508 # Hot path: most specifiers and small SpecifierSets reduce to 

509 # a single contiguous range. 

510 lower, upper = ranges[0] 

511 above = lower._above 

512 below = upper._below 

513 for item in iterable: 

514 parsed = _coerce_version(item if key is None else key(item)) 

515 if parsed is None: 

516 continue 

517 if exclude_prereleases and parsed.is_prerelease: 

518 continue 

519 if above is not None and not above(parsed): 

520 continue 

521 if below is None or below(parsed): 

522 yield item 

523 return 

524 

525 for item in iterable: 

526 parsed = _coerce_version(item if key is None else key(item)) 

527 if parsed is None: 

528 continue 

529 if exclude_prereleases and parsed.is_prerelease: 

530 continue 

531 for lower, upper in ranges: 

532 above = lower._above 

533 if above is not None and not above(parsed): 

534 break 

535 below = upper._below 

536 if below is None or below(parsed): 

537 yield item 

538 break 

539 

540 

541def _lowest_release_at_or_above( 

542 value: _VersionOrBoundary, 

543) -> Version | None: 

544 """Smallest non-pre-release version at or above *value*, or None.""" 

545 if value is None: 

546 return None 

547 if isinstance(value, _BoundaryVersion): 

548 inner_version = value.version 

549 if inner_version.is_prerelease: 

550 # AFTER_LOCALS(1.0a1) -> nearest non-pre is 1.0 

551 return inner_version.__replace__(pre=None, dev=None, local=None) 

552 # AFTER_LOCALS(1.0) -> nearest non-pre is 1.0.post0 

553 # AFTER_LOCALS(1.0.post0) -> nearest non-pre is 1.0.post1 

554 next_post = (inner_version.post + 1) if inner_version.post is not None else 0 

555 return inner_version.__replace__(post=next_post, local=None) 

556 if not value.is_prerelease: 

557 return value 

558 # Strip pre/dev to get the final or post-release form. 

559 return value.__replace__(pre=None, dev=None, local=None) 

560 

561 

562def ranges_are_prerelease_only(ranges: Sequence[VersionRange]) -> bool: 

563 """True when every range in *ranges* contains only pre-releases. 

564 

565 Used to detect unsatisfiable specifier sets when ``prereleases=False``: 

566 if every range is pre-release-only, every contained version is excluded. 

567 """ 

568 for lower, upper in ranges: 

569 nearest = _lowest_release_at_or_above(lower.version) 

570 if nearest is None: 

571 return False 

572 if upper.version is None or nearest < upper.version: 

573 return False 

574 if nearest == upper.version and upper.inclusive: 

575 return False 

576 return True 

577 

578 

579def wildcard_ranges(op: str, base: Version) -> list[VersionRange]: 

580 """Ranges for ==V.* and !=V.*. 

581 

582 ==1.2.* -> [1.2.dev0, 1.3.dev0); !=1.2.* -> complement. 

583 """ 

584 lower = _base_dev0(base) 

585 upper = _next_prefix_dev0(base) 

586 if op == "==": 

587 return [(_LowerBound(lower, True), _UpperBound(upper, False))] 

588 # != 

589 return [ 

590 (_NEG_INF, _UpperBound(lower, False)), 

591 (_LowerBound(upper, True), _POS_INF), 

592 ] 

593 

594 

595def standard_ranges(op: str, version: Version, has_local: bool) -> list[VersionRange]: 

596 """Ranges for the standard PEP 440 operators (no wildcard, no ===). 

597 

598 *has_local* indicates whether the spec string included a ``+local`` 

599 segment; relevant only for ``==`` / ``!=`` to decide whether the 

600 upper bound includes V's local family. 

601 """ 

602 if op == ">=": 

603 return [(_LowerBound(version, True), _POS_INF)] 

604 

605 if op == "<=": 

606 return [ 

607 ( 

608 _NEG_INF, 

609 _UpperBound( 

610 _BoundaryVersion(version, _BoundaryKind.AFTER_LOCALS), True 

611 ), 

612 ) 

613 ] 

614 

615 if op == ">": 

616 if version.dev is not None: 

617 # >V.devN: dev versions have no post-releases, so the 

618 # next real version is V.dev(N+1). 

619 lower_bound = version.__replace__(dev=version.dev + 1, local=None) 

620 return [(_LowerBound(lower_bound, True), _POS_INF)] 

621 if version.post is not None: 

622 # >V.postN: next real version is V.post(N+1).dev0. 

623 lower_bound = version.__replace__(post=version.post + 1, dev=0, local=None) 

624 return [(_LowerBound(lower_bound, True), _POS_INF)] 

625 # >V (final or pre-release V): exclude V itself, V+local, and 

626 # every V.postN per PEP 440. 

627 return [ 

628 ( 

629 _LowerBound( 

630 _BoundaryVersion(version, _BoundaryKind.AFTER_POSTS), False 

631 ), 

632 _POS_INF, 

633 ) 

634 ] 

635 

636 if op == "<": 

637 # <V excludes pre-releases of V when V is not a pre-release. 

638 # V.dev0 is the earliest pre-release of V. 

639 bound = ( 

640 version if version.is_prerelease else version.__replace__(dev=0, local=None) 

641 ) 

642 if bound <= _MIN_VERSION: 

643 return [] 

644 return [(_NEG_INF, _UpperBound(bound, False))] 

645 

646 # ==, !=: local versions of V match when the spec has no local segment. 

647 after_locals = _BoundaryVersion(version, _BoundaryKind.AFTER_LOCALS) 

648 upper = version if has_local else after_locals 

649 

650 if op == "==": 

651 return [(_LowerBound(version, True), _UpperBound(upper, True))] 

652 

653 if op == "!=": 

654 return [ 

655 (_NEG_INF, _UpperBound(version, False)), 

656 (_LowerBound(upper, False), _POS_INF), 

657 ] 

658 

659 if op == "~=": 

660 prefix = version.__replace__(release=version.release[:-1]) 

661 return [ 

662 (_LowerBound(version, True), _UpperBound(_next_prefix_dev0(prefix), False)) 

663 ] 

664 

665 raise ValueError(f"Unknown operator: {op!r}") # pragma: no cover