cachetools 7.2.6__tar.gz → 7.2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {cachetools-7.2.6 → cachetools-7.2.7}/CHANGELOG.rst +8 -0
  2. {cachetools-7.2.6/src/cachetools.egg-info → cachetools-7.2.7}/PKG-INFO +1 -1
  3. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools/__init__.py +58 -60
  4. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools/_descriptor.py +43 -37
  5. {cachetools-7.2.6 → cachetools-7.2.7/src/cachetools.egg-info}/PKG-INFO +1 -1
  6. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_cachedmethod.py +73 -0
  7. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_classmethod.py +43 -0
  8. {cachetools-7.2.6 → cachetools-7.2.7}/LICENSE +0 -0
  9. {cachetools-7.2.6 → cachetools-7.2.7}/MANIFEST.in +0 -0
  10. {cachetools-7.2.6 → cachetools-7.2.7}/README.rst +0 -0
  11. {cachetools-7.2.6 → cachetools-7.2.7}/docs/conf.py +0 -0
  12. {cachetools-7.2.6 → cachetools-7.2.7}/docs/index.rst +0 -0
  13. {cachetools-7.2.6 → cachetools-7.2.7}/pyproject.toml +0 -0
  14. {cachetools-7.2.6 → cachetools-7.2.7}/setup.cfg +0 -0
  15. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools/__init__.pyi +0 -0
  16. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools/_cached.py +0 -0
  17. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools/_cachedmethod.py +0 -0
  18. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools/func.py +0 -0
  19. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools/func.pyi +0 -0
  20. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools/keys.py +0 -0
  21. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools/keys.pyi +0 -0
  22. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools/py.typed +0 -0
  23. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools.egg-info/SOURCES.txt +0 -0
  24. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools.egg-info/dependency_links.txt +0 -0
  25. {cachetools-7.2.6 → cachetools-7.2.7}/src/cachetools.egg-info/top_level.txt +0 -0
  26. {cachetools-7.2.6 → cachetools-7.2.7}/tests/__init__.py +0 -0
  27. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_cache.py +0 -0
  28. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_cached.py +0 -0
  29. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_fifo.py +0 -0
  30. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_func.py +0 -0
  31. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_keys.py +0 -0
  32. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_lfu.py +0 -0
  33. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_lru.py +0 -0
  34. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_rr.py +0 -0
  35. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_threading.py +0 -0
  36. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_tlru.py +0 -0
  37. {cachetools-7.2.6 → cachetools-7.2.7}/tests/test_ttl.py +0 -0
  38. {cachetools-7.2.6 → cachetools-7.2.7}/tox.ini +0 -0
@@ -1,3 +1,11 @@
1
+ v7.2.7 (2026-10-28)
2
+ ===================
3
+
4
+ - Various performance improvements.
5
+
6
+ - Minor unit test improvements.
7
+
8
+
1
9
  v7.2.6 (2026-10-26)
2
10
  ===================
3
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.6
3
+ Version: 7.2.7
4
4
  Summary: Extensible memoizing collections and decorators
5
5
  Author-email: Thomas Kemmer <tkemmer@computer.org>
6
6
  Maintainer-email: Thomas Kemmer <tkemmer@computer.org>
@@ -12,7 +12,7 @@ __all__ = (
12
12
  "cachedmethod",
13
13
  )
14
14
 
15
- __version__ = "7.2.6"
15
+ __version__ = "7.2.7"
16
16
 
17
17
  import collections
18
18
  import collections.abc
@@ -24,30 +24,12 @@ import time
24
24
  from . import keys
25
25
 
26
26
 
27
- class _DefaultSize:
28
- """A minimal "fake" dict that returns a constant size 1 for any key."""
29
-
30
- __slots__ = ()
31
-
32
- def __getitem__(self, _key):
33
- return 1
34
-
35
- def __setitem__(self, _key, _value):
36
- pass
37
-
38
- def pop(self, _key):
39
- return 1
40
-
41
- def clear(self):
42
- pass
43
-
44
-
45
27
  class Cache(collections.abc.MutableMapping):
46
28
  """Mutable mapping to serve as a simple cache or cache base class."""
47
29
 
48
30
  __marker = object()
49
31
 
50
- __size = _DefaultSize()
32
+ __size = None
51
33
 
52
34
  def __init__(self, maxsize, getsizeof=None):
53
35
  if maxsize < 0:
@@ -75,30 +57,48 @@ class Cache(collections.abc.MutableMapping):
75
57
  return self.__missing__(key)
76
58
 
77
59
  def __setitem__(self, key, value):
60
+ data = self.__data
61
+ size = self.__size
78
62
  maxsize = self.__maxsize
79
- size = self.getsizeof(value)
80
- if size < 0:
81
- raise ValueError("value size must be non-negative")
82
- if size > maxsize:
83
- raise ValueError("value too large")
84
- if key not in self.__data:
85
- diffsize = size
86
- while self.__currsize + diffsize > maxsize:
87
- self.popitem()
63
+ if size is None:
64
+ if maxsize < 1:
65
+ raise ValueError("value too large")
66
+ elif key in data:
67
+ data[key] = value
68
+ else:
69
+ while self.__currsize + 1 > maxsize:
70
+ self.popitem()
71
+ data[key] = value
72
+ self.__currsize += 1
88
73
  else:
89
- diffsize = size - self.__size[key]
90
- while self.__currsize + diffsize > maxsize:
91
- self.popitem()
92
- if key not in self.__data:
93
- diffsize = size
94
- self.__data[key] = value
95
- self.__size[key] = size
96
- self.__currsize += diffsize
74
+ itemsize = self.getsizeof(value)
75
+ if itemsize > maxsize:
76
+ raise ValueError("value too large")
77
+ if itemsize < 0:
78
+ raise ValueError("value size must be non-negative")
79
+ if key not in data:
80
+ diffsize = itemsize
81
+ while self.__currsize + diffsize > maxsize:
82
+ self.popitem()
83
+ else:
84
+ diffsize = itemsize - size[key]
85
+ while self.__currsize + diffsize > maxsize:
86
+ self.popitem()
87
+ if key not in data:
88
+ diffsize = itemsize
89
+ data[key] = value
90
+ size[key] = itemsize
91
+ self.__currsize += diffsize
97
92
 
98
93
  def __delitem__(self, key):
99
- size = self.__size.pop(key)
100
- del self.__data[key]
101
- self.__currsize -= size
94
+ size = self.__size
95
+ if size is None:
96
+ del self.__data[key]
97
+ self.__currsize -= 1
98
+ else:
99
+ itemsize = size.pop(key)
100
+ del self.__data[key]
101
+ self.__currsize -= itemsize
102
102
 
103
103
  def __contains__(self, key):
104
104
  return key in self.__data
@@ -148,8 +148,10 @@ class Cache(collections.abc.MutableMapping):
148
148
  # optimized version for each Cache subclass.
149
149
 
150
150
  def clear(self):
151
+ size = self.__size
152
+ if size is not None:
153
+ size.clear()
151
154
  self.__data.clear()
152
- self.__size.clear()
153
155
  self.__currsize = 0
154
156
 
155
157
  @property
@@ -508,11 +510,12 @@ class TTLCache(_TimedCache):
508
510
  expires = time + self.__ttl
509
511
  self.expire(time)
510
512
  cache_setitem(self, key, value)
511
- try:
512
- link = self.__getlink(key)
513
- except KeyError:
514
- self.__links[key] = link = TTLCache._Link(key)
513
+ links = self.__links
514
+ link = links.get(key)
515
+ if link is None:
516
+ links[key] = link = TTLCache._Link(key)
515
517
  else:
518
+ links.move_to_end(key)
516
519
  link.unlink()
517
520
  link.expires = expires
518
521
  link.next = root = self.__root
@@ -560,6 +563,8 @@ class TTLCache(_TimedCache):
560
563
  time = self.timer()
561
564
  root = self.__root
562
565
  curr = root.next
566
+ if curr is root or time < curr.expires:
567
+ return [] # fast path: nothing has expired
563
568
  links = self.__links
564
569
  expired = []
565
570
  cache_delitem = Cache.__delitem__
@@ -595,11 +600,6 @@ class TTLCache(_TimedCache):
595
600
  root.prev = root.next = root
596
601
  self.__links.clear()
597
602
 
598
- def __getlink(self, key):
599
- value = self.__links[key]
600
- self.__links.move_to_end(key)
601
- return value
602
-
603
603
 
604
604
  class TLRUCache(_TimedCache):
605
605
  """Time-aware Least Recently Used (TLRU) cache implementation."""
@@ -655,11 +655,12 @@ class TLRUCache(_TimedCache):
655
655
  cache_setitem(self, key, value)
656
656
  # removing an existing item would break the heap structure, so
657
657
  # only mark it as removed for now
658
- try:
659
- self.__getitem(key).removed = True
660
- except KeyError:
661
- pass
662
- self.__items[key] = item = TLRUCache._Item(key, expires)
658
+ items = self.__items
659
+ item = items.get(key)
660
+ if item is not None:
661
+ item.removed = True
662
+ items.move_to_end(key)
663
+ items[key] = item = TLRUCache._Item(key, expires)
663
664
  heapq.heappush(self.__order, item)
664
665
 
665
666
  def __delitem__(self, key, cache_delitem=Cache.__delitem__):
@@ -696,6 +697,8 @@ class TLRUCache(_TimedCache):
696
697
  if len(order) > len(items) * self.__HEAP_CLEANUP_FACTOR:
697
698
  self.__order = order = [item for item in order if not item.removed]
698
699
  heapq.heapify(order)
700
+ if not order or (not order[0].removed and time < order[0].expires):
701
+ return [] # fast path: nothing has expired
699
702
  expired = []
700
703
  cache_delitem = Cache.__delitem__
701
704
  cache_getitem = Cache.__getitem__
@@ -728,11 +731,6 @@ class TLRUCache(_TimedCache):
728
731
  self.__items.clear()
729
732
  del self.__order[:]
730
733
 
731
- def __getitem(self, key):
732
- value = self.__items[key]
733
- self.__items.move_to_end(key)
734
- return value
735
-
736
734
  def __delitem(self, key, cache_delitem=Cache.__delitem__):
737
735
  try:
738
736
  self.__items.pop(key).removed = True
@@ -46,46 +46,34 @@ class _MethodDescriptor:
46
46
  # objtype is not used for instance lookups, so it need not be bound here
47
47
  return self.__get_unbound()
48
48
  elif self.__attrname is not None:
49
- # replace descriptor instance with wrapper in instance dict
50
- wrapper = self.Wrapper(obj, self) # type: ignore
51
- try:
52
- descriptor = next(
53
- (
54
- cls.__dict__[self.__attrname]
55
- for cls in type(obj).__mro__
56
- if self.__attrname in cls.__dict__
57
- ),
58
- None,
59
- )
60
- if descriptor is self:
61
- # In case of a race condition where another thread already replaced
62
- # the descriptor, prefer the initial wrapper.
49
+ descriptor = next(
50
+ (
51
+ cls.__dict__[self.__attrname]
52
+ for cls in type(obj).__mro__
53
+ if self.__attrname in cls.__dict__
54
+ ),
55
+ None,
56
+ )
57
+ if descriptor is self:
58
+ # In case of a race condition where another thread already replaced
59
+ # the descriptor, prefer the initial wrapper.
60
+ wrapper = self.Wrapper(obj, self) # type: ignore
61
+ try:
63
62
  wrapper = obj.__dict__.setdefault(self.__attrname, wrapper)
64
- else:
65
- # A super() lookup must neither reuse the override's wrapper
66
- # nor replace it, so preserve state separately for each descriptor.
63
+ except (AttributeError, TypeError) as e:
64
+ self.__instance_dict_error(obj, e)
65
+ else:
66
+ # A super() lookup must neither reuse the override's wrapper
67
+ # nor replace it, so preserve state separately for each descriptor.
68
+ try:
67
69
  wrappers = obj.__dict__.setdefault("__cachetools_cachedmethods", {})
70
+ except (AttributeError, TypeError) as e:
71
+ self.__instance_dict_error(obj, e)
72
+ wrappers = {} # deprecated fallback: throwaway wrapper
73
+ wrapper = wrappers.get(self)
74
+ if wrapper is None:
75
+ wrapper = self.Wrapper(obj, self) # type: ignore
68
76
  wrapper = wrappers.setdefault(self, wrapper)
69
- except AttributeError:
70
- # not all objects have __dict__ (e.g. class defines slots)
71
- msg = (
72
- f"No '__dict__' attribute on {type(obj).__name__!r} "
73
- f"instance to cache {self.__attrname!r} property."
74
- )
75
- if self.__deprecated:
76
- _warn_instance_dict(msg, 3)
77
- else:
78
- raise TypeError(msg) from None
79
- except TypeError:
80
- msg = (
81
- f"The '__dict__' attribute on {type(obj).__name__!r} "
82
- f"instance does not support item assignment for "
83
- f"caching {self.__attrname!r} property."
84
- )
85
- if self.__deprecated:
86
- _warn_instance_dict(msg, 3)
87
- else:
88
- raise TypeError(msg) from None
89
77
  elif self.__deprecated:
90
78
  # deprecated @classmethod, warning already raised elsewhere
91
79
  wrapper = self.Wrapper(obj, self) # type: ignore
@@ -110,6 +98,24 @@ class _MethodDescriptor:
110
98
  self.__unbound = _Unbound(self)
111
99
  return self.__unbound
112
100
 
101
+ def __instance_dict_error(self, obj, error):
102
+ if isinstance(error, AttributeError):
103
+ # not all objects have __dict__ (e.g. class defines slots)
104
+ msg = (
105
+ f"No '__dict__' attribute on {type(obj).__name__!r} "
106
+ f"instance to cache {self.__attrname!r}"
107
+ )
108
+ else:
109
+ # some objects may have an immutable __dict__
110
+ msg = (
111
+ f"The '__dict__' attribute on {type(obj).__name__!r} "
112
+ f"instance does not support item assignment for "
113
+ f"caching {self.__attrname!r}"
114
+ )
115
+ if not self.__deprecated:
116
+ raise TypeError(msg) from None
117
+ _warn_instance_dict(msg, 4)
118
+
113
119
 
114
120
  class _Unbound:
115
121
  """Callable returned on class access.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.6
3
+ Version: 7.2.7
4
4
  Summary: Extensible memoizing collections and decorators
5
5
  Author-email: Thomas Kemmer <tkemmer@computer.org>
6
6
  Maintainer-email: Thomas Kemmer <tkemmer@computer.org>
@@ -565,6 +565,15 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
565
565
  def get_info(self, value):
566
566
  return value
567
567
 
568
+ class SlotsChild(Slots):
569
+ __slots__ = ()
570
+
571
+ def get(self, value): # type: ignore
572
+ return super().get(value)
573
+
574
+ def get_info(self, value): # type: ignore
575
+ return super().get_info(value)
576
+
568
577
  cache = self.cache(2)
569
578
  cached = Slots(cache)
570
579
 
@@ -579,6 +588,21 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
579
588
  self.assertEqual(len(w), 2)
580
589
  self.assertIs(w[0].category, DeprecationWarning)
581
590
  self.assertIs(w[1].category, DeprecationWarning)
591
+ self.assertEqual(w[0].filename, __file__)
592
+
593
+ child = SlotsChild(cache)
594
+
595
+ with self.assertRaisesRegex(TypeError, "No '__dict__' attribute"):
596
+ child.get_info(42)
597
+
598
+ with warnings.catch_warnings(record=True) as w:
599
+ warnings.simplefilter("always")
600
+ self.assertEqual(child.get(0), 0)
601
+ self.assertEqual(child.get(0), 0)
602
+ self.assertEqual(len(w), 2)
603
+ self.assertIs(w[0].category, DeprecationWarning)
604
+ self.assertIs(w[1].category, DeprecationWarning)
605
+ self.assertEqual(w[0].filename, __file__)
582
606
 
583
607
  def test_decorator_immutable_dict(self):
584
608
 
@@ -602,6 +626,13 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
602
626
  def get_info(self, value):
603
627
  return value
604
628
 
629
+ class ImmutableChild(Immutable):
630
+ def get(self, value): # type: ignore
631
+ return super().get(value)
632
+
633
+ def get_info(self, value): # type: ignore
634
+ return super().get_info(value)
635
+
605
636
  cache = self.cache(2)
606
637
  cached = Immutable(cache)
607
638
 
@@ -616,6 +647,48 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
616
647
  self.assertEqual(len(w), 2)
617
648
  self.assertIs(w[0].category, DeprecationWarning)
618
649
  self.assertIs(w[1].category, DeprecationWarning)
650
+ self.assertEqual(w[0].filename, __file__)
651
+
652
+ child = ImmutableChild(cache)
653
+
654
+ with self.assertRaisesRegex(TypeError, "does not support item assignment"):
655
+ child.get_info(42)
656
+
657
+ with warnings.catch_warnings(record=True) as w:
658
+ warnings.simplefilter("always")
659
+ self.assertEqual(child.get(0), 0)
660
+ self.assertEqual(child.get(0), 0)
661
+ self.assertEqual(len(w), 2)
662
+ self.assertIs(w[0].category, DeprecationWarning)
663
+ self.assertIs(w[1].category, DeprecationWarning)
664
+ self.assertEqual(w[0].filename, __file__)
665
+
666
+ def test_decorator_wrapper_error(self):
667
+ # a non-callable key makes Wrapper.__init__ raise TypeError
668
+ class Cached:
669
+ def __init__(self, cache):
670
+ self.cache = cache
671
+
672
+ @cachedmethod(lambda self: self.cache, key=None) # type: ignore
673
+ def get(self, value):
674
+ return value
675
+
676
+ @cachedmethod(lambda self: self.cache, key=None, info=True) # type: ignore
677
+ def get_info(self, value):
678
+ return value
679
+
680
+ class CachedChild(Cached):
681
+ def get(self, value): # type: ignore
682
+ return super().get(value)
683
+
684
+ def get_info(self, value):
685
+ return super().get_info(value)
686
+
687
+ cached = CachedChild(self.cache(2))
688
+ with self.assertRaisesRegex(TypeError, "must be callable"):
689
+ cached.get(0)
690
+ with self.assertRaisesRegex(TypeError, "must be callable"):
691
+ cached.get_info(0)
619
692
 
620
693
  def test_decorator_different_names(self):
621
694
  # RuntimeError for Python < 3.12, TypeError otherwise
@@ -36,6 +36,12 @@ class Cached:
36
36
  cls.count += 1
37
37
  return cls.count
38
38
 
39
+ @classmethod
40
+ @cachedmethod(lambda cls: cls.cache, info=True)
41
+ def get_info(cls, value):
42
+ cls.count += 1
43
+ return cls.count
44
+
39
45
 
40
46
  class CachedClassMethodTest(unittest.TestCase):
41
47
  def test(self):
@@ -114,6 +120,43 @@ class CachedClassMethodTest(unittest.TestCase):
114
120
  self.assertIs(w[0].category, DeprecationWarning)
115
121
  self.assertIs(w[1].category, DeprecationWarning)
116
122
 
123
+ def test_info(self):
124
+ # info=True requires an instance method, so calls fail on all
125
+ # supported Python versions
126
+ Cached.cache = LRUCache(2)
127
+ cached = Cached()
128
+
129
+ with warnings.catch_warnings():
130
+ warnings.simplefilter("ignore")
131
+ with self.assertRaises(TypeError):
132
+ cached.get_info(0)
133
+ with self.assertRaises(TypeError):
134
+ Cached.get_info(0)
135
+ self.assertEqual(len(Cached.cache), 0)
136
+
137
+ def test_shared_cache(self):
138
+ class Child(Cached):
139
+ pass
140
+
141
+ # cache is a class attribute, so it is shared with subclasses
142
+ Cached.cache = LRUCache(2)
143
+ Cached.count = 0
144
+
145
+ with warnings.catch_warnings():
146
+ warnings.simplefilter("ignore")
147
+ self.assertIs(Child.cache, Cached.cache)
148
+ self.assertEqual(Cached.get(0), 1)
149
+ self.assertEqual(Child.get(0), 1)
150
+ self.assertEqual(Child().get(0), 1)
151
+ self.assertEqual(len(Cached.cache), 1)
152
+ self.assertEqual(Child.get(1), 2)
153
+ self.assertEqual(Cached.get(1), 2)
154
+ self.assertEqual(len(Cached.cache), 2)
155
+
156
+ Child.get.cache_clear(Child) # type: ignore
157
+ self.assertEqual(len(Cached.cache), 0)
158
+ self.assertEqual(Cached.get(0), 2)
159
+
117
160
  def test_clear(self):
118
161
  Cached.cache = LRUCache(2)
119
162
  Cached.count = 0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes