Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/cachetools/__init__.py: 73%

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

552 statements  

1"""Extensible memoizing collections and decorators.""" 

2 

3__all__ = ( 

4 "Cache", 

5 "CacheInfo", 

6 "FIFOCache", 

7 "LFUCache", 

8 "LRUCache", 

9 "RRCache", 

10 "TLRUCache", 

11 "TTLCache", 

12 "cached", 

13 "cachedmethod", 

14) 

15 

16__version__ = "8.0.0" 

17 

18import collections 

19import collections.abc 

20import functools 

21import heapq 

22import random 

23import time 

24 

25from . import keys 

26 

27 

28class Cache(collections.abc.MutableMapping): 

29 """Mutable mapping to serve as a simple cache or cache base class.""" 

30 

31 __marker = object() 

32 

33 __size = None 

34 

35 def __init__(self, maxsize, getsizeof=None): 

36 if maxsize < 0: 

37 raise ValueError("maxsize must be non-negative") 

38 if getsizeof is not None: 

39 self.getsizeof = getsizeof 

40 if self.getsizeof is not Cache.getsizeof: 

41 self.__size = {} 

42 self.__data = {} 

43 self.__currsize = 0 

44 self.__maxsize = maxsize 

45 

46 def __repr__(self): 

47 return "%s(%s, maxsize=%r, currsize=%r)" % ( 

48 type(self).__name__, 

49 repr(self.__data), 

50 self.__maxsize, 

51 self.__currsize, 

52 ) 

53 

54 def __getitem__(self, key): 

55 try: 

56 return self.__data[key] 

57 except KeyError: 

58 return self.__missing__(key) 

59 

60 def __setitem__(self, key, value): 

61 data = self.__data 

62 size = self.__size 

63 maxsize = self.__maxsize 

64 if size is None: 

65 if maxsize < 1: 

66 raise ValueError("value too large") 

67 elif key in data: 

68 data[key] = value 

69 else: 

70 while self.__currsize + 1 > maxsize: 

71 self.popitem() 

72 data[key] = value 

73 self.__currsize += 1 

74 else: 

75 itemsize = self.getsizeof(value) 

76 if itemsize > maxsize: 

77 raise ValueError("value too large") 

78 if itemsize < 0: 

79 raise ValueError("value size must be non-negative") 

80 if key not in data: 

81 diffsize = itemsize 

82 while self.__currsize + diffsize > maxsize: 

83 self.popitem() 

84 else: 

85 diffsize = itemsize - size[key] 

86 while self.__currsize + diffsize > maxsize: 

87 self.popitem() 

88 if key not in data: 

89 diffsize = itemsize 

90 data[key] = value 

91 size[key] = itemsize 

92 self.__currsize += diffsize 

93 

94 def __delitem__(self, key): 

95 size = self.__size 

96 if size is None: 

97 del self.__data[key] 

98 self.__currsize -= 1 

99 else: 

100 itemsize = size.pop(key) 

101 del self.__data[key] 

102 self.__currsize -= itemsize 

103 

104 def __contains__(self, key): 

105 return key in self.__data 

106 

107 def __missing__(self, key): 

108 raise KeyError(key) 

109 

110 def __iter__(self): 

111 return iter(self.__data) 

112 

113 def __len__(self): 

114 return len(self.__data) 

115 

116 # Note that we cannot simply inherit get(), pop() and setdefault() 

117 # from MutableMapping, since these rely on __getitem__ throwing a 

118 # KeyError on cache miss. This is not the case if __missing__ is 

119 # implemented for a Cache subclass, so we have to roll our own, 

120 # somewhat less elegant versions. 

121 

122 def get(self, key, default=None): 

123 if key in self: 

124 return self[key] 

125 else: 

126 return default 

127 

128 def pop(self, key, default=__marker): 

129 if key in self: 

130 value = self[key] 

131 del self[key] 

132 elif default is self.__marker: 

133 raise KeyError(key) 

134 else: 

135 value = default 

136 return value 

137 

138 def setdefault(self, key, default=None): 

139 if key in self: 

140 value = self[key] 

141 else: 

142 self[key] = value = default 

143 return value 

144 

145 # Although the MutableMapping.clear() default implementation works 

146 # perfectly well, it calls popitem() in a loop until the cache is 

147 # empty, resulting in O(n) complexity. For large caches, this 

148 # becomes a significant performance bottleneck, so we provide an 

149 # optimized version for each Cache subclass. 

150 

151 def clear(self): 

152 size = self.__size 

153 if size is not None: 

154 size.clear() 

155 self.__data.clear() 

156 self.__currsize = 0 

157 

158 @property 

159 def maxsize(self): 

160 """The maximum size of the cache.""" 

161 return self.__maxsize 

162 

163 @property 

164 def currsize(self): 

165 """The current size of the cache.""" 

166 return self.__currsize 

167 

168 @staticmethod 

169 def getsizeof(value): 

170 """Return the size of a cache element's value.""" 

171 return 1 

172 

173 

174class FIFOCache(Cache): 

175 """First In First Out (FIFO) cache implementation.""" 

176 

177 def __init__(self, maxsize, getsizeof=None): 

178 Cache.__init__(self, maxsize, getsizeof) 

179 self.__order = collections.OrderedDict() 

180 

181 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): 

182 cache_setitem(self, key, value) 

183 if key in self.__order: 

184 self.__order.move_to_end(key) 

185 else: 

186 self.__order[key] = None 

187 

188 def __delitem__(self, key, cache_delitem=Cache.__delitem__): 

189 cache_delitem(self, key) 

190 del self.__order[key] 

191 

192 def popitem(self): 

193 """Remove and return the `(key, value)` pair first inserted.""" 

194 try: 

195 key = next(iter(self.__order)) 

196 except StopIteration: 

197 raise KeyError("%s is empty" % type(self).__name__) from None 

198 else: 

199 value = Cache.__getitem__(self, key) 

200 del self[key] 

201 return (key, value) 

202 

203 def clear(self): 

204 Cache.clear(self) 

205 self.__order.clear() 

206 

207 

208class LFUCache(Cache): 

209 """Least Frequently Used (LFU) cache implementation.""" 

210 

211 class _Link: 

212 __slots__ = ("count", "keys", "next", "prev") 

213 

214 def __init__(self, count): 

215 self.count = count 

216 self.keys = collections.OrderedDict() 

217 

218 def unlink(self): 

219 next = self.next 

220 prev = self.prev 

221 prev.next = next 

222 next.prev = prev 

223 

224 def __init__(self, maxsize, getsizeof=None): 

225 Cache.__init__(self, maxsize, getsizeof) 

226 self.__root = root = LFUCache._Link(0) # sentinel 

227 root.prev = root.next = root 

228 self.__links = {} 

229 

230 def __getitem__(self, key, cache_getitem=Cache.__getitem__): 

231 value = cache_getitem(self, key) 

232 if key in self.__links: # __missing__ may not store item 

233 self.__touch(key) 

234 return value 

235 

236 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): 

237 cache_setitem(self, key, value) 

238 if key in self.__links: 

239 self.__touch(key) 

240 return 

241 root = self.__root 

242 link = root.next 

243 if link.count != 1: 

244 link = LFUCache._Link(1) 

245 link.next = root.next 

246 root.next = link.next.prev = link 

247 link.prev = root 

248 link.keys[key] = None 

249 self.__links[key] = link 

250 

251 def __delitem__(self, key, cache_delitem=Cache.__delitem__): 

252 cache_delitem(self, key) 

253 link = self.__links.pop(key) 

254 del link.keys[key] 

255 if not link.keys: 

256 link.unlink() 

257 

258 def popitem(self): 

259 """Remove and return the `(key, value)` pair least frequently used.""" 

260 root = self.__root 

261 curr = root.next 

262 if curr is root: 

263 raise KeyError("%s is empty" % type(self).__name__) from None 

264 key = next(iter(curr.keys)) # remove the "oldest" element 

265 value = Cache.__getitem__(self, key) 

266 del self[key] 

267 return (key, value) 

268 

269 def clear(self): 

270 Cache.clear(self) 

271 root = self.__root 

272 root.prev = root.next = root 

273 self.__links.clear() 

274 

275 def __touch(self, key): 

276 """Increment use count.""" 

277 link = self.__links[key] 

278 curr = link.next 

279 if curr.count != link.count + 1: 

280 if len(link.keys) == 1: 

281 link.count += 1 

282 return 

283 curr = LFUCache._Link(link.count + 1) 

284 curr.next = link.next 

285 link.next = curr.next.prev = curr 

286 curr.prev = link 

287 curr.keys[key] = None 

288 del link.keys[key] 

289 if not link.keys: 

290 link.unlink() 

291 self.__links[key] = curr 

292 

293 

294class LRUCache(Cache): 

295 """Least Recently Used (LRU) cache implementation.""" 

296 

297 def __init__(self, maxsize, getsizeof=None): 

298 Cache.__init__(self, maxsize, getsizeof) 

299 self.__order = collections.OrderedDict() 

300 

301 def __getitem__(self, key, cache_getitem=Cache.__getitem__): 

302 value = cache_getitem(self, key) 

303 try: 

304 self.__order.move_to_end(key) 

305 except KeyError: # __missing__ may not store item 

306 pass 

307 return value 

308 

309 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): 

310 cache_setitem(self, key, value) 

311 self.__touch(key) 

312 

313 def __delitem__(self, key, cache_delitem=Cache.__delitem__): 

314 cache_delitem(self, key) 

315 del self.__order[key] 

316 

317 def popitem(self): 

318 """Remove and return the `(key, value)` pair least recently used.""" 

319 try: 

320 key = next(iter(self.__order)) 

321 except StopIteration: 

322 raise KeyError("%s is empty" % type(self).__name__) from None 

323 else: 

324 value = Cache.__getitem__(self, key) 

325 del self[key] 

326 return (key, value) 

327 

328 def clear(self): 

329 Cache.clear(self) 

330 self.__order.clear() 

331 

332 def __touch(self, key): 

333 """Mark as recently used.""" 

334 try: 

335 self.__order.move_to_end(key) 

336 except KeyError: 

337 self.__order[key] = None 

338 

339 

340class RRCache(Cache): 

341 """Random Replacement (RR) cache implementation.""" 

342 

343 def __init__(self, maxsize, choice=random.choice, getsizeof=None): 

344 Cache.__init__(self, maxsize, getsizeof) 

345 self.__choice = choice 

346 self.__index = {} 

347 self.__keys = [] 

348 

349 @property 

350 def choice(self): 

351 """The `choice` function used by the cache.""" 

352 return self.__choice 

353 

354 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): 

355 cache_setitem(self, key, value) 

356 if key not in self.__index: 

357 self.__index[key] = len(self.__keys) 

358 self.__keys.append(key) 

359 

360 def __delitem__(self, key, cache_delitem=Cache.__delitem__): 

361 cache_delitem(self, key) 

362 index = self.__index.pop(key) 

363 if index != len(self.__keys) - 1: 

364 last = self.__keys[-1] 

365 self.__keys[index] = last 

366 self.__index[last] = index 

367 self.__keys.pop() 

368 

369 def popitem(self): 

370 """Remove and return a random `(key, value)` pair.""" 

371 keys = self.__keys 

372 if not keys: 

373 raise KeyError("%s is empty" % type(self).__name__) from None 

374 key = self.__choice(keys) 

375 value = Cache.__getitem__(self, key) 

376 del self[key] 

377 return (key, value) 

378 

379 def clear(self): 

380 Cache.clear(self) 

381 self.__index.clear() 

382 del self.__keys[:] 

383 

384 

385class _TimedCache(Cache): 

386 """Base class for time-aware cache implementations.""" 

387 

388 class _Timer: 

389 def __init__(self, timer): 

390 self.__timer = timer 

391 self.__nesting = 0 

392 

393 def __call__(self): 

394 if self.__nesting == 0: 

395 return self.__timer() 

396 else: 

397 return self.__time 

398 

399 def __enter__(self): 

400 if self.__nesting == 0: 

401 self.__time = time = self.__timer() 

402 else: 

403 time = self.__time 

404 self.__nesting += 1 

405 return time 

406 

407 def __exit__(self, *exc): 

408 self.__nesting -= 1 

409 

410 def __reduce__(self): 

411 return _TimedCache._Timer, (self.__timer,) 

412 

413 def __getattr__(self, name): 

414 return getattr(self.__timer, name) 

415 

416 def __init__(self, maxsize, timer, getsizeof=None): 

417 Cache.__init__(self, maxsize, getsizeof) 

418 self.__timer = _TimedCache._Timer(timer) 

419 

420 def __repr__(self, cache_repr=Cache.__repr__): 

421 with self.__timer as time: 

422 self.expire(time) 

423 return cache_repr(self) 

424 

425 def __len__(self, cache_len=Cache.__len__): 

426 with self.__timer as time: 

427 self.expire(time) 

428 return cache_len(self) 

429 

430 @property 

431 def currsize(self): 

432 with self.__timer as time: 

433 self.expire(time) 

434 return super().currsize 

435 

436 @property 

437 def timer(self): 

438 """The timer function used by the cache.""" 

439 return self.__timer 

440 

441 def get(self, *args, **kwargs): 

442 with self.__timer: 

443 return Cache.get(self, *args, **kwargs) 

444 

445 def pop(self, *args, **kwargs): 

446 with self.__timer: 

447 return Cache.pop(self, *args, **kwargs) 

448 

449 def setdefault(self, *args, **kwargs): 

450 with self.__timer: 

451 return Cache.setdefault(self, *args, **kwargs) 

452 

453 def clear(self): 

454 # Subclasses must override to also reset their own time-tracking 

455 # structures; we do not call expire() here since clear() should 

456 # be O(1) regardless of cache contents. 

457 Cache.clear(self) 

458 

459 def expire(self, time=None): # pragma: no cover 

460 raise NotImplementedError 

461 

462 

463class TTLCache(_TimedCache): 

464 """LRU Cache implementation with per-item time-to-live (TTL) value.""" 

465 

466 class _Link: 

467 __slots__ = ("expires", "key", "next", "prev") 

468 

469 def __init__(self, key=None, expires=None): 

470 self.key = key 

471 self.expires = expires 

472 

473 def __reduce__(self): 

474 return TTLCache._Link, (self.key, self.expires) 

475 

476 def unlink(self): 

477 next = self.next 

478 prev = self.prev 

479 prev.next = next 

480 next.prev = prev 

481 

482 def __init__(self, maxsize, ttl, timer=time.monotonic, getsizeof=None): 

483 _TimedCache.__init__(self, maxsize, timer, getsizeof) 

484 self.__root = root = TTLCache._Link() 

485 root.prev = root.next = root 

486 self.__links = collections.OrderedDict() 

487 self.__ttl = ttl 

488 

489 def __contains__(self, key): 

490 try: 

491 link = self.__links[key] # no reordering 

492 except KeyError: 

493 return False 

494 else: 

495 return self.timer() < link.expires 

496 

497 def __getitem__(self, key, cache_getitem=Cache.__getitem__): 

498 links = self.__links 

499 try: 

500 link = links[key] 

501 except KeyError: 

502 return cache_getitem(self, key) 

503 links.move_to_end(key) 

504 if self.timer() < link.expires: 

505 return cache_getitem(self, key) 

506 else: 

507 return self.__missing__(key) 

508 

509 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): 

510 with self.timer as time: 

511 expires = time + self.__ttl 

512 self.expire(time) 

513 cache_setitem(self, key, value) 

514 links = self.__links 

515 link = links.get(key) 

516 if link is None: 

517 links[key] = link = TTLCache._Link(key) 

518 else: 

519 links.move_to_end(key) 

520 link.unlink() 

521 link.expires = expires 

522 link.next = root = self.__root 

523 link.prev = prev = root.prev 

524 prev.next = root.prev = link 

525 

526 def __delitem__(self, key, cache_delitem=Cache.__delitem__): 

527 cache_delitem(self, key) 

528 link = self.__links.pop(key) 

529 link.unlink() 

530 if not (self.timer() < link.expires): 

531 raise KeyError(key) 

532 

533 def __iter__(self): 

534 root = self.__root 

535 curr = root.next 

536 while curr is not root: 

537 # "freeze" time for iterator access 

538 with self.timer as time: 

539 if time < curr.expires: 

540 yield curr.key 

541 curr = curr.next 

542 

543 def __setstate__(self, state): 

544 self.__dict__.update(state) 

545 root = self.__root 

546 root.prev = root.next = root 

547 for link in sorted(self.__links.values(), key=lambda obj: obj.expires): 

548 link.next = root 

549 link.prev = prev = root.prev 

550 prev.next = root.prev = link 

551 self.expire(self.timer()) 

552 

553 @property 

554 def ttl(self): 

555 """The time-to-live value of the cache's items.""" 

556 return self.__ttl 

557 

558 def expire(self, time=None): 

559 """Remove expired items from the cache and return an iterable of the 

560 expired `(key, value)` pairs. 

561 

562 """ 

563 if time is None: 

564 time = self.timer() 

565 root = self.__root 

566 curr = root.next 

567 if curr is root or time < curr.expires: 

568 return [] # fast path: nothing has expired 

569 links = self.__links 

570 expired = [] 

571 cache_delitem = Cache.__delitem__ 

572 cache_getitem = Cache.__getitem__ 

573 while curr is not root and not (time < curr.expires): 

574 expired.append((curr.key, cache_getitem(self, curr.key))) 

575 cache_delitem(self, curr.key) 

576 del links[curr.key] 

577 next = curr.next 

578 curr.unlink() 

579 curr = next 

580 return expired 

581 

582 def popitem(self): 

583 """Remove and return the `(key, value)` pair least recently used that 

584 has not already expired. 

585 

586 """ 

587 with self.timer as time: 

588 self.expire(time) 

589 try: 

590 key = next(iter(self.__links)) 

591 except StopIteration: 

592 raise KeyError("%s is empty" % type(self).__name__) from None 

593 else: 

594 value = Cache.__getitem__(self, key) 

595 del self[key] 

596 return (key, value) 

597 

598 def clear(self): 

599 _TimedCache.clear(self) 

600 root = self.__root 

601 root.prev = root.next = root 

602 self.__links.clear() 

603 

604 

605class TLRUCache(_TimedCache): 

606 """Time-aware Least Recently Used (TLRU) cache implementation.""" 

607 

608 __HEAP_CLEANUP_FACTOR = 2 # clean up the heap if size > N * len(items) 

609 

610 @functools.total_ordering 

611 class _Item: 

612 __slots__ = ("expires", "key", "removed") 

613 

614 def __init__(self, key=None, expires=None): 

615 self.key = key 

616 self.expires = expires 

617 self.removed = False 

618 

619 def __lt__(self, other): 

620 return self.expires < other.expires 

621 

622 def __init__(self, maxsize, ttu, timer=time.monotonic, getsizeof=None): 

623 _TimedCache.__init__(self, maxsize, timer, getsizeof) 

624 self.__items = collections.OrderedDict() 

625 self.__order = [] 

626 self.__ttu = ttu 

627 

628 def __contains__(self, key): 

629 try: 

630 item = self.__items[key] # no reordering 

631 except KeyError: 

632 return False 

633 else: 

634 return self.timer() < item.expires 

635 

636 def __getitem__(self, key, cache_getitem=Cache.__getitem__): 

637 items = self.__items 

638 try: 

639 item = items[key] 

640 except KeyError: 

641 return cache_getitem(self, key) 

642 items.move_to_end(key) 

643 if self.timer() < item.expires: 

644 return cache_getitem(self, key) 

645 else: 

646 return self.__missing__(key) 

647 

648 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): 

649 with self.timer as time: 

650 self.expire(time) 

651 expires = self.__ttu(key, value, time) 

652 if not (time < expires): 

653 # updating an existing item with an already expired 

654 # one should remove the existing item 

655 return self.__delitem(key) 

656 cache_setitem(self, key, value) 

657 # removing an existing item would break the heap structure, so 

658 # only mark it as removed for now 

659 items = self.__items 

660 item = items.get(key) 

661 if item is not None: 

662 item.removed = True 

663 items.move_to_end(key) 

664 items[key] = item = TLRUCache._Item(key, expires) 

665 heapq.heappush(self.__order, item) 

666 

667 def __delitem__(self, key, cache_delitem=Cache.__delitem__): 

668 with self.timer as time: 

669 # no self.expire() for performance reasons, e.g. self.clear() [#67] 

670 cache_delitem(self, key) 

671 item = self.__items.pop(key) 

672 item.removed = True 

673 if not (time < item.expires): 

674 raise KeyError(key) 

675 

676 def __iter__(self): 

677 for curr in self.__order: 

678 # "freeze" time for iterator access 

679 with self.timer as time: 

680 if time < curr.expires and not curr.removed: 

681 yield curr.key 

682 

683 @property 

684 def ttu(self): 

685 """The time-to-use function used by the cache.""" 

686 return self.__ttu 

687 

688 def expire(self, time=None): 

689 """Remove expired items from the cache and return an iterable of the 

690 expired `(key, value)` pairs. 

691 

692 """ 

693 if time is None: 

694 time = self.timer() 

695 items = self.__items 

696 order = self.__order 

697 # clean up the heap if too many items are marked as removed 

698 if len(order) > len(items) * self.__HEAP_CLEANUP_FACTOR: 

699 self.__order = order = [item for item in order if not item.removed] 

700 heapq.heapify(order) 

701 if not order or (not order[0].removed and time < order[0].expires): 

702 return [] # fast path: nothing has expired 

703 expired = [] 

704 cache_delitem = Cache.__delitem__ 

705 cache_getitem = Cache.__getitem__ 

706 while order and (order[0].removed or not (time < order[0].expires)): 

707 item = heapq.heappop(order) 

708 if not item.removed: 

709 expired.append((item.key, cache_getitem(self, item.key))) 

710 cache_delitem(self, item.key) 

711 del items[item.key] 

712 return expired 

713 

714 def popitem(self): 

715 """Remove and return the `(key, value)` pair least recently used that 

716 has not already expired. 

717 

718 """ 

719 with self.timer as time: 

720 self.expire(time) 

721 try: 

722 key = next(iter(self.__items)) 

723 except StopIteration: 

724 raise KeyError("%s is empty" % type(self).__name__) from None 

725 else: 

726 value = Cache.__getitem__(self, key) 

727 del self[key] 

728 return (key, value) 

729 

730 def clear(self): 

731 _TimedCache.clear(self) 

732 self.__items.clear() 

733 del self.__order[:] 

734 

735 def __delitem(self, key, cache_delitem=Cache.__delitem__): 

736 try: 

737 self.__items.pop(key).removed = True 

738 except KeyError: 

739 pass 

740 else: 

741 cache_delitem(self, key) 

742 

743 

744class CacheInfo(collections.namedtuple("CacheInfo", "hits misses maxsize currsize")): 

745 """Named tuple holding cache statistics.""" 

746 

747 __slots__ = () 

748 

749 @classmethod 

750 def _make_info(cls, cache, hits, misses): 

751 if isinstance(cache, Cache): 

752 return cls(hits, misses, cache.maxsize, cache.currsize) 

753 else: 

754 return cls(hits, misses, None, len(cache)) 

755 

756 

757def cached(cache, key=keys.hashkey, lock=None, condition=None, info=False): 

758 """Decorator to wrap a function with a memoizing callable that saves 

759 results in a cache. 

760 

761 """ 

762 from ._cached import _wrapper 

763 

764 if cache is None: # no longer supported since v8.0.0 

765 raise TypeError("cache must not be None") 

766 

767 def decorator(func): 

768 make_info = CacheInfo._make_info if info else None 

769 return _wrapper(func, cache, key, lock, condition, make_info) 

770 

771 return decorator 

772 

773 

774def cachedmethod(cache, key=keys.methodkey, lock=None, condition=None, info=False): 

775 """Decorator to wrap a method with a memoizing callable that saves 

776 results in a cache. 

777 

778 """ 

779 from ._cachedmethod import _wrapper 

780 

781 def decorator(method): 

782 make_info = CacheInfo._make_info if info else None 

783 return _wrapper(method, cache, key, lock, condition, make_info) 

784 

785 return decorator