Whole file
tkem/cachetools
The author described this change as “Fix #159: Pass self to @cachedmethod key function.”. It counts as a record because the check below fails on the code as it stood at 0e778e441 and passes on 9dda91f99, with nothing else changed between the two runs.
Projecttkem/cachetools
Fix saved2021-12-21
Sharing licenceMIT · LICENSE
Change size+678 −674
What the code was meant to do, written into the code itself as a save note
Fix #159: Pass self to @cachedmethod key function.
The change
| 22 | 22 | import random | |
| 23 | 23 | import time | |
| 24 | 24 | ||
| 25 | - | from .keys import hashkey | |
| 26 | - | ||
| 27 | - | ||
| 28 | - | class _DefaultSize: | |
| 29 | - | ||
| 30 | - | __slots__ = () | |
| 31 | - | ||
| 32 | - | def __getitem__(self, _): | |
| 33 | - | return 1 | |
| 34 | - | ||
| 35 | - | def __setitem__(self, _, value): | |
| 36 | - | assert value == 1 | |
| 37 | - | ||
| 38 | - | def pop(self, _): | |
| 39 | - | return 1 | |
| 40 | - | ||
| 41 | - | ||
| 42 | - | class Cache(collections.abc.MutableMapping): | |
| 43 | - | """Mutable mapping to serve as a simple cache or cache base class.""" | |
| 44 | - | ||
| 45 | - | __marker = object() | |
| 46 | - | ||
| 47 | - | __size = _DefaultSize() | |
| 48 | - | ||
| 49 | - | def __init__(self, maxsize, getsizeof=None): | |
| 50 | - | if getsizeof: | |
| 51 | - | self.getsizeof = getsizeof | |
| 52 | - | if self.getsizeof is not Cache.getsizeof: | |
| 53 | - | self.__size = dict() | |
| 54 | - | self.__data = dict() | |
| 55 | - | self.__currsize = 0 | |
| 56 | - | self.__maxsize = maxsize | |
| 57 | - | ||
| 58 | - | def __repr__(self): | |
| 59 | - | return "%s(%s, maxsize=%r, currsize=%r)" % ( | |
| 60 | - | self.__class__.__name__, | |
| 61 | - | repr(self.__data), | |
| 62 | - | self.__maxsize, | |
| 63 | - | self.__currsize, | |
| 64 | - | ) | |
| 65 | - | ||
| 66 | - | def __getitem__(self, key): | |
| 67 | - | try: | |
| 68 | - | return self.__data[key] | |
| 69 | - | except KeyError: | |
| 70 | - | return self.__missing__(key) | |
| 71 | - | ||
| 72 | - | def __setitem__(self, key, value): | |
| 73 | - | maxsize = self.__maxsize | |
| 74 | - | size = self.getsizeof(value) | |
| 75 | - | if size > maxsize: | |
| 76 | - | raise ValueError("value too large") | |
| 77 | - | if key not in self.__data or self.__size[key] < size: | |
| 78 | - | while self.__currsize + size > maxsize: | |
| 79 | - | self.popitem() | |
| 80 | - | if key in self.__data: | |
| 81 | - | diffsize = size - self.__size[key] | |
| 82 | - | else: | |
| 83 | - | diffsize = size | |
| 84 | - | self.__data[key] = value | |
| 85 | - | self.__size[key] = size | |
| 86 | - | self.__currsize += diffsize | |
| 87 | - | ||
| 88 | - | def __delitem__(self, key): | |
| 89 | - | size = self.__size.pop(key) | |
| 90 | - | del self.__data[key] | |
| 91 | - | self.__currsize -= size | |
| 92 | - | ||
| 93 | - | def __contains__(self, key): | |
| 94 | - | return key in self.__data | |
| 95 | - | ||
| 96 | - | def __missing__(self, key): | |
| 97 | - | raise KeyError(key) | |
| 98 | - | ||
| 99 | - | def __iter__(self): | |
| 100 | - | return iter(self.__data) | |
| 101 | - | ||
| 102 | - | def __len__(self): | |
| 103 | - | return len(self.__data) | |
| 104 | - | ||
| 105 | - | def get(self, key, default=None): | |
| 106 | - | if key in self: | |
| 107 | - | return self[key] | |
| 108 | - | else: | |
| 109 | - | return default | |
| 110 | - | ||
| 111 | - | def pop(self, key, default=__marker): | |
| 112 | - | if key in self: | |
| 113 | - | value = self[key] | |
| 114 | - | del self[key] | |
| 115 | - | elif default is self.__marker: | |
| 116 | - | raise KeyError(key) | |
| 117 | - | else: | |
| 118 | - | value = default | |
| 119 | - | return value | |
| 120 | - | ||
| 121 | - | def setdefault(self, key, default=None): | |
| 122 | - | if key in self: | |
| 123 | - | value = self[key] | |
| 124 | - | else: | |
| 125 | - | self[key] = value = default | |
| 126 | - | return value | |
| 127 | - | ||
| 128 | - | @property | |
| 129 | - | def maxsize(self): | |
| 130 | - | """The maximum size of the cache.""" | |
| 131 | - | return self.__maxsize | |
| 132 | - | ||
| 133 | - | @property | |
| 134 | - | def currsize(self): | |
| 135 | - | """The current size of the cache.""" | |
| 136 | - | return self.__currsize | |
| 137 | - | ||
| 138 | - | @staticmethod | |
| 139 | - | def getsizeof(value): | |
| 140 | - | """Return the size of a cache element's value.""" | |
| 141 | - | return 1 | |
| 142 | - | ||
| 143 | - | ||
| 144 | - | class FIFOCache(Cache): | |
| 145 | - | """First In First Out (FIFO) cache implementation.""" | |
| 146 | - | ||
| 147 | - | def __init__(self, maxsize, getsizeof=None): | |
| 148 | - | Cache.__init__(self, maxsize, getsizeof) | |
| 149 | - | self.__order = collections.OrderedDict() | |
| 150 | - | ||
| 151 | - | def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): | |
| 152 | - | cache_setitem(self, key, value) | |
| 153 | - | try: | |
| 154 | - | self.__order.move_to_end(key) | |
| 155 | - | except KeyError: | |
| 156 | - | self.__order[key] = None | |
| 157 | - | ||
| 158 | - | def __delitem__(self, key, cache_delitem=Cache.__delitem__): | |
| 159 | - | cache_delitem(self, key) | |
| 160 | - | del self.__order[key] | |
| 161 | - | ||
| 162 | - | def popitem(self): | |
| 163 | - | """Remove and return the `(key, value)` pair first inserted.""" | |
| 164 | - | try: | |
| 165 | - | key = next(iter(self.__order)) | |
| 166 | - | except StopIteration: | |
| 167 | - | raise KeyError("%s is empty" % type(self).__name__) from None | |
| 168 | - | else: | |
| 169 | - | return (key, self.pop(key)) | |
| 170 | - | ||
| 171 | - | ||
| 172 | - | class LFUCache(Cache): | |
| 173 | - | """Least Frequently Used (LFU) cache implementation.""" | |
| 174 | - | ||
| 175 | - | def __init__(self, maxsize, getsizeof=None): | |
| 176 | - | Cache.__init__(self, maxsize, getsizeof) | |
| 177 | - | self.__counter = collections.Counter() | |
| 178 | - | ||
| 179 | - | def __getitem__(self, key, cache_getitem=Cache.__getitem__): | |
| 180 | - | value = cache_getitem(self, key) | |
| 181 | - | if key in self: # __missing__ may not store item | |
| 182 | - | self.__counter[key] -= 1 | |
| 183 | - | return value | |
| 184 | - | ||
| 185 | - | def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): | |
| 186 | - | cache_setitem(self, key, value) | |
| 187 | - | self.__counter[key] -= 1 | |
| 188 | - | ||
| 189 | - | def __delitem__(self, key, cache_delitem=Cache.__delitem__): | |
| 190 | - | cache_delitem(self, key) | |
| 191 | - | del self.__counter[key] | |
| 192 | - | ||
| 193 | - | def popitem(self): | |
| 194 | - | """Remove and return the `(key, value)` pair least frequently used.""" | |
| 195 | - | try: | |
| 196 | - | ((key, _),) = self.__counter.most_common(1) | |
| 197 | - | except ValueError: | |
| 198 | - | raise KeyError("%s is empty" % type(self).__name__) from None | |
| 199 | - | else: | |
| 200 | - | return (key, self.pop(key)) | |
| 201 | - | ||
| 202 | - | ||
| 203 | - | class LRUCache(Cache): | |
| 204 | - | """Least Recently Used (LRU) cache implementation.""" | |
| 205 | - | ||
| 206 | - | def __init__(self, maxsize, getsizeof=None): | |
| 207 | - | Cache.__init__(self, maxsize, getsizeof) | |
| 208 | - | self.__order = collections.OrderedDict() | |
| 209 | - | ||
| 210 | - | def __getitem__(self, key, cache_getitem=Cache.__getitem__): | |
| 211 | - | value = cache_getitem(self, key) | |
| 212 | - | if key in self: # __missing__ may not store item | |
| 213 | - | self.__update(key) | |
| 214 | - | return value | |
| 215 | - | ||
| 216 | - | def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): | |
| 217 | - | cache_setitem(self, key, value) | |
| 218 | - | self.__update(key) | |
| 219 | - | ||
| 220 | - | def __delitem__(self, key, cache_delitem=Cache.__delitem__): | |
| 221 | - | cache_delitem(self, key) | |
| 222 | - | del self.__order[key] | |
| 223 | - | ||
| 224 | - | def popitem(self): | |
| 225 | - | """Remove and return the `(key, value)` pair least recently used.""" | |
| 226 | - | try: | |
| 227 | - | key = next(iter(self.__order)) | |
| 228 | - | except StopIteration: | |
| 229 | - | raise KeyError("%s is empty" % type(self).__name__) from None | |
| 230 | - | else: | |
| 231 | - | return (key, self.pop(key)) | |
| 232 | - | ||
| 233 | - | def __update(self, key): | |
| 234 | - | try: | |
| 235 | - | self.__order.move_to_end(key) | |
| 236 | - | except KeyError: | |
| 237 | - | self.__order[key] = None | |
| 238 | - | ||
| 239 | - | ||
| 240 | - | class MRUCache(Cache): | |
| 241 | - | """Most Recently Used (MRU) cache implementation.""" | |
| 242 | - | ||
| 243 | - | def __init__(self, maxsize, getsizeof=None): | |
| 244 | - | Cache.__init__(self, maxsize, getsizeof) | |
| 245 | - | self.__order = collections.OrderedDict() | |
| 246 | - | ||
| 247 | - | def __getitem__(self, key, cache_getitem=Cache.__getitem__): | |
| 248 | - | value = cache_getitem(self, key) | |
| 249 | - | if key in self: # __missing__ may not store item | |
| 250 | - | self.__update(key) | |
| 251 | - | return value | |
| 252 | - | ||
| 253 | - | def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): | |
| 254 | - | cache_setitem(self, key, value) | |
| 255 | - | self.__update(key) | |
| 256 | - | ||
| 257 | - | def __delitem__(self, key, cache_delitem=Cache.__delitem__): | |
| 258 | - | cache_delitem(self, key) | |
| 259 | - | del self.__order[key] | |
| 260 | - | ||
| 261 | - | def popitem(self): | |
| 262 | - | """Remove and return the `(key, value)` pair most recently used.""" | |
| 263 | - | try: | |
| 264 | - | key = next(iter(self.__order)) | |
| 265 | - | except StopIteration: | |
| 266 | - | raise KeyError("%s is empty" % type(self).__name__) from None | |
| 267 | - | else: | |
| 268 | - | return (key, self.pop(key)) | |
| 269 | - | ||
| 270 | - | def __update(self, key): | |
| 271 | - | try: | |
| 272 | - | self.__order.move_to_end(key, last=False) | |
| 273 | - | except KeyError: | |
| 274 | - | self.__order[key] = None | |
| 275 | - | ||
| 276 | - | ||
| 277 | - | class RRCache(Cache): | |
| 278 | - | """Random Replacement (RR) cache implementation.""" | |
| 279 | - | ||
| 280 | - | def __init__(self, maxsize, choice=random.choice, getsizeof=None): | |
| 281 | - | Cache.__init__(self, maxsize, getsizeof) | |
| 282 | - | self.__choice = choice | |
| 283 | - | ||
| 284 | - | @property | |
| 285 | - | def choice(self): | |
| 286 | - | """The `choice` function used by the cache.""" | |
| 287 | - | return self.__choice | |
| 288 | - | ||
| 289 | - | def popitem(self): | |
| 290 | - | """Remove and return a random `(key, value)` pair.""" | |
| 291 | - | try: | |
| 292 | - | key = self.__choice(list(self)) | |
| 293 | - | except IndexError: | |
| 294 | - | raise KeyError("%s is empty" % type(self).__name__) from None | |
| 295 | - | else: | |
| 296 | - | return (key, self.pop(key)) | |
| 297 | - | ||
| 298 | - | ||
| 299 | - | class _TimedCache(Cache): | |
| 300 | - | """Base class for time aware cache implementations.""" | |
| 301 | - | ||
| 302 | - | class _Timer: | |
| 303 | - | def __init__(self, timer): | |
| 304 | - | self.__timer = timer | |
| 305 | - | self.__nesting = 0 | |
| 306 | - | ||
| 307 | - | def __call__(self): | |
| 308 | - | if self.__nesting == 0: | |
| 309 | - | return self.__timer() | |
| 310 | - | else: | |
| 311 | - | return self.__time | |
| 312 | - | ||
| 313 | - | def __enter__(self): | |
| 314 | - | if self.__nesting == 0: | |
| 315 | - | self.__time = time = self.__timer() | |
| 316 | - | else: | |
| 317 | - | time = self.__time | |
| 318 | - | self.__nesting += 1 | |
| 319 | - | return time | |
| 320 | - | ||
| 321 | - | def __exit__(self, *exc): | |
| 322 | - | self.__nesting -= 1 | |
| 323 | - | ||
| 324 | - | def __reduce__(self): | |
| 325 | - | return _TimedCache._Timer, (self.__timer,) | |
| 326 | - | ||
| 327 | - | def __getattr__(self, name): | |
| 328 | - | return getattr(self.__timer, name) | |
| 329 | - | ||
| 330 | - | def __init__(self, maxsize, timer=time.monotonic, getsizeof=None): | |
| 331 | - | Cache.__init__(self, maxsize, getsizeof) | |
| 332 | - | self.__timer = _TimedCache._Timer(timer) | |
| 333 | - | ||
| 334 | - | def __repr__(self, cache_repr=Cache.__repr__): | |
| 335 | - | with self.__timer as time: | |
| 336 | - | self.expire(time) | |
| 337 | - | return cache_repr(self) | |
| 338 | - | ||
| 339 | - | def __len__(self, cache_len=Cache.__len__): | |
| 340 | - | with self.__timer as time: | |
| 341 | - | self.expire(time) | |
| 342 | - | return cache_len(self) | |
| 343 | - | ||
| 344 | - | @property | |
| 345 | - | def currsize(self): | |
| 346 | - | with self.__timer as time: | |
| 347 | - | self.expire(time) | |
| 348 | - | return super().currsize | |
| 349 | - | ||
| 350 | - | @property | |
| 351 | - | def timer(self): | |
| 352 | - | """The timer function used by the cache.""" | |
| 353 | - | return self.__timer | |
| 354 | - | ||
| 355 | - | def clear(self): | |
| 356 | - | with self.__timer as time: | |
| 357 | - | self.expire(time) | |
| 358 | - | Cache.clear(self) | |
| 359 | - | ||
| 360 | - | def get(self, *args, **kwargs): | |
| 361 | - | with self.__timer: | |
| 362 | - | return Cache.get(self, *args, **kwargs) | |
| 363 | - | ||
| 364 | - | def pop(self, *args, **kwargs): | |
| 365 | - | with self.__timer: | |
| 366 | - | return Cache.pop(self, *args, **kwargs) | |
| 367 | - | ||
| 368 | - | def setdefault(self, *args, **kwargs): | |
| 369 | - | with self.__timer: | |
| 370 | - | return Cache.setdefault(self, *args, **kwargs) | |
| 371 | - | ||
| 372 | - | ||
| 373 | - | class TTLCache(_TimedCache): | |
| 374 | - | """LRU Cache implementation with per-item time-to-live (TTL) value.""" | |
| 375 | - | ||
| 376 | - | class _Link: | |
| 377 | - | ||
| 378 | - | __slots__ = ("key", "expires", "next", "prev") | |
| 379 | - | ||
| 380 | - | def __init__(self, key=None, expires=None): | |
| 381 | - | self.key = key | |
| 382 | - | self.expires = expires | |
| 383 | - | ||
| 384 | - | def __reduce__(self): | |
| 385 | - | return TTLCache._Link, (self.key, self.expires) | |
| 386 | - | ||
| 387 | - | def unlink(self): | |
| 388 | - | next = self.next | |
| 389 | - | prev = self.prev | |
| 390 | - | prev.next = next | |
| 391 | - | next.prev = prev | |
| 392 | - | ||
| 393 | - | def __init__(self, maxsize, ttl, timer=time.monotonic, getsizeof=None): | |
| 394 | - | _TimedCache.__init__(self, maxsize, timer, getsizeof) | |
| 395 | - | self.__root = root = TTLCache._Link() | |
| 396 | - | root.prev = root.next = root | |
| 397 | - | self.__links = collections.OrderedDict() | |
| 398 | - | self.__ttl = ttl | |
| 399 | - | ||
| 400 | - | def __contains__(self, key): | |
| 401 | - | try: | |
| 402 | - | link = self.__links[key] # no reordering | |
| 403 | - | except KeyError: | |
| 404 | - | return False | |
| 405 | - | else: | |
| 406 | - | return self.timer() < link.expires | |
| 407 | - | ||
| 408 | - | def __getitem__(self, key, cache_getitem=Cache.__getitem__): | |
| 409 | - | try: | |
| 410 | - | link = self.__getlink(key) | |
| 411 | - | except KeyError: | |
| 412 | - | expired = False | |
| 413 | - | else: | |
| 414 | - | expired = not (self.timer() < link.expires) | |
| 415 | - | if expired: | |
| 416 | - | return self.__missing__(key) | |
| 417 | - | else: | |
| 418 | - | return cache_getitem(self, key) | |
| 419 | - | ||
| 420 | - | def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): | |
| 421 | - | with self.timer as time: | |
| 958 further changed lines not shown | |||
The check that tells the two apart
fail→pass·tests/test_method.py::CachedMethodTest::test_unhashable
Check file tests/test_method.py, taken without changes from the fix and copied onto the older code, so the exact same check runs against both versions.
Origin and history
The code before it0e778e4410641af906930877c14f12f592d16fe2
Broken version dated2021-12-19
Modulecachetools.__init__
Units changedcached, cachedmethod
Fingerprint7d2f314202d4573d
Checked2026-08-18 by goldset/0.1
Every field above is generated by our program. None of it is written by hand.
Other bugs found in tkem/cachetools
- 2026-07-22TLRUCache
- 2026-03-08Fix #218: Fix and properly document @cachedmethod.cache_key handling.
- 2026-03-05_DescriptorBase
- 2024-08-18Fix #292, fix #205, fix #103: TTLCache.expire() returns iterable of expired (key, value) pairs.
- 2024-07-15mru_cache
- 2021-12-18TTLCache