cachetools 7.2.4__tar.gz → 7.2.5__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.
- {cachetools-7.2.4 → cachetools-7.2.5}/CHANGELOG.rst +31 -15
- {cachetools-7.2.4/src/cachetools.egg-info → cachetools-7.2.5}/PKG-INFO +1 -1
- {cachetools-7.2.4 → cachetools-7.2.5}/docs/index.rst +91 -87
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools/__init__.py +6 -6
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools/__init__.pyi +8 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools/_cachedmethod.py +22 -14
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools/_descriptor.py +47 -6
- {cachetools-7.2.4 → cachetools-7.2.5/src/cachetools.egg-info}/PKG-INFO +1 -1
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_cachedmethod.py +209 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/LICENSE +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/MANIFEST.in +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/README.rst +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/docs/conf.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/pyproject.toml +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/setup.cfg +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools/_cached.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools/func.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools/func.pyi +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools/keys.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools/keys.pyi +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools/py.typed +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools.egg-info/SOURCES.txt +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools.egg-info/dependency_links.txt +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/src/cachetools.egg-info/top_level.txt +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/__init__.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_cache.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_cached.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_classmethod.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_fifo.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_func.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_keys.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_lfu.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_lru.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_rr.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_threading.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_tlru.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tests/test_ttl.py +0 -0
- {cachetools-7.2.4 → cachetools-7.2.5}/tox.ini +0 -0
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
v7.2.5 (2026-10-25)
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
- Fix ``@cachedmethod`` with ``condition`` for decorated methods
|
|
5
|
+
taking a ``pending`` keyword argument (broken since v7.0.0).
|
|
6
|
+
|
|
7
|
+
- Fix pickling and deep-copying of instances using ``@cachedmethod``
|
|
8
|
+
(broken since v7.0.0).
|
|
9
|
+
|
|
10
|
+
- Fix calling ``@cachedmethod`` decorated methods through the class
|
|
11
|
+
with the receiver (``self``) passed as a keyword argument (broken
|
|
12
|
+
since v7.2.4).
|
|
13
|
+
|
|
14
|
+
- Various documentation improvements.
|
|
15
|
+
|
|
16
|
+
|
|
1
17
|
v7.2.4 (2026-10-19)
|
|
2
18
|
===================
|
|
3
19
|
|
|
@@ -162,7 +178,7 @@ v7.0.3 (2026-03-05)
|
|
|
162
178
|
v7.0.2 (2026-03-02)
|
|
163
179
|
===================
|
|
164
180
|
|
|
165
|
-
- Provide more efficient ``clear()`` implementation for all
|
|
181
|
+
- Provide more efficient ``clear()`` implementation for all supported
|
|
166
182
|
Cache classes (courtesy Josep Pon Farreny).
|
|
167
183
|
|
|
168
184
|
|
|
@@ -226,7 +242,7 @@ v6.2.5 (2026-01-25)
|
|
|
226
242
|
v6.2.4 (2025-12-15)
|
|
227
243
|
===================
|
|
228
244
|
|
|
229
|
-
- Fix license information displayed on PyPI
|
|
245
|
+
- Fix license information displayed on PyPI by using an updated
|
|
230
246
|
version of ``twine`` for uploading.
|
|
231
247
|
|
|
232
248
|
|
|
@@ -299,7 +315,7 @@ v6.0.0 (2025-05-23)
|
|
|
299
315
|
- Convert the ``cachetools.func`` decorators to use a
|
|
300
316
|
``threading.Condition`` instance to deal with `cache stampede
|
|
301
317
|
<https://en.wikipedia.org/wiki/Cache_stampede>`_ issues. Note that
|
|
302
|
-
this *may* result in a
|
|
318
|
+
this *may* result in a noticeable performance degradation, depending
|
|
303
319
|
on your actual use case.
|
|
304
320
|
|
|
305
321
|
- Deprecate support for ``cache(self)`` returning ``None`` to suppress
|
|
@@ -424,7 +440,7 @@ v5.0.0 (2021-12-21)
|
|
|
424
440
|
|
|
425
441
|
``from cachetools.ttl import TTLCache``
|
|
426
442
|
|
|
427
|
-
will no longer work.
|
|
443
|
+
will no longer work. Use
|
|
428
444
|
|
|
429
445
|
``from cachetools import TTLCache``
|
|
430
446
|
|
|
@@ -541,7 +557,7 @@ v3.1.1 (2019-05-23)
|
|
|
541
557
|
|
|
542
558
|
- Document how to use shared caches with ``@cachedmethod``.
|
|
543
559
|
|
|
544
|
-
- Fix pickling/unpickling of cache keys
|
|
560
|
+
- Fix pickling/unpickling of cache keys.
|
|
545
561
|
|
|
546
562
|
|
|
547
563
|
v3.1.0 (2019-01-29)
|
|
@@ -670,17 +686,17 @@ v1.1.0 (2015-08-28)
|
|
|
670
686
|
|
|
671
687
|
- Add ``hashkey`` and ``typedkey`` functions.
|
|
672
688
|
|
|
673
|
-
- Add
|
|
689
|
+
- Add ``key`` and ``lock`` arguments to ``@cachedmethod``.
|
|
674
690
|
|
|
675
691
|
- Set ``__wrapped__`` attributes for Python versions < 3.2.
|
|
676
692
|
|
|
677
693
|
- Move ``functools`` compatible decorators to ``cachetools.func``.
|
|
678
694
|
|
|
679
|
-
- Deprecate ``@cachedmethod``
|
|
695
|
+
- Deprecate ``@cachedmethod`` ``typed`` argument.
|
|
680
696
|
|
|
681
|
-
- Deprecate
|
|
697
|
+
- Deprecate ``cache`` attribute for ``@cachedmethod`` wrappers.
|
|
682
698
|
|
|
683
|
-
- Deprecate
|
|
699
|
+
- Deprecate ``getsizeof`` and ``lock`` arguments for ``cachetools.func``
|
|
684
700
|
decorator.
|
|
685
701
|
|
|
686
702
|
|
|
@@ -739,13 +755,13 @@ v0.8.0 (2014-12-03)
|
|
|
739
755
|
|
|
740
756
|
- Add ``Cache.__missing__()``.
|
|
741
757
|
|
|
742
|
-
- Feature freeze for
|
|
758
|
+
- Feature freeze for ``v1.0``.
|
|
743
759
|
|
|
744
760
|
|
|
745
761
|
v0.7.1 (2014-11-22)
|
|
746
762
|
===================
|
|
747
763
|
|
|
748
|
-
- Fix
|
|
764
|
+
- Fix ``MANIFEST.in``.
|
|
749
765
|
|
|
750
766
|
|
|
751
767
|
v0.7.0 (2014-11-12)
|
|
@@ -753,7 +769,7 @@ v0.7.0 (2014-11-12)
|
|
|
753
769
|
|
|
754
770
|
- Deprecate ``TTLCache.ExpiredError``.
|
|
755
771
|
|
|
756
|
-
- Add
|
|
772
|
+
- Add ``choice`` argument to ``RRCache`` constructor.
|
|
757
773
|
|
|
758
774
|
- Refactor ``LFUCache``, ``LRUCache`` and ``TTLCache``.
|
|
759
775
|
|
|
@@ -768,7 +784,7 @@ v0.6.0 (2014-10-13)
|
|
|
768
784
|
|
|
769
785
|
- Support unsynchronized function decorators.
|
|
770
786
|
|
|
771
|
-
- Allow ``@cachedmethod.cache()`` to return None
|
|
787
|
+
- Allow ``@cachedmethod.cache()`` to return ``None``.
|
|
772
788
|
|
|
773
789
|
|
|
774
790
|
v0.5.1 (2014-09-25)
|
|
@@ -796,7 +812,7 @@ v0.4.0 (2014-06-16)
|
|
|
796
812
|
|
|
797
813
|
- Add ``Cache`` base class.
|
|
798
814
|
|
|
799
|
-
- Remove ``@cachedmethod``
|
|
815
|
+
- Remove ``@cachedmethod`` ``lock`` parameter.
|
|
800
816
|
|
|
801
817
|
|
|
802
818
|
v0.3.1 (2014-05-07)
|
|
@@ -804,7 +820,7 @@ v0.3.1 (2014-05-07)
|
|
|
804
820
|
|
|
805
821
|
- Add proper locking for ``cache_clear()`` and ``cache_info()``.
|
|
806
822
|
|
|
807
|
-
- Report
|
|
823
|
+
- Report ``size`` in ``cache_info()``.
|
|
808
824
|
|
|
809
825
|
|
|
810
826
|
v0.3.0 (2014-05-06)
|
|
@@ -36,21 +36,23 @@ Cache implementations
|
|
|
36
36
|
This module provides several classes implementing caches using
|
|
37
37
|
different cache algorithms. All these classes derive from class
|
|
38
38
|
:class:`Cache`, which in turn derives from
|
|
39
|
-
:class:`collections.abc.MutableMapping`, and provide
|
|
40
|
-
and :attr
|
|
39
|
+
:class:`collections.abc.MutableMapping`, and provide
|
|
40
|
+
:attr:`~Cache.maxsize` and :attr:`~Cache.currsize` properties to
|
|
41
|
+
retrieve the maximum and current
|
|
41
42
|
size of the cache. When a cache is full, :meth:`Cache.__setitem__()`
|
|
42
43
|
calls :meth:`self.popitem()` repeatedly until there is enough room for
|
|
43
44
|
the item to be added.
|
|
44
45
|
|
|
45
|
-
In general, a cache's size is the total size of its
|
|
46
|
-
Therefore, :class:`Cache` provides a :meth
|
|
46
|
+
In general, a cache's size is the total size of its items' values.
|
|
47
|
+
Therefore, :class:`Cache` provides a :meth:`~Cache.getsizeof` method, which
|
|
47
48
|
returns the size of a given `value`. The default implementation of
|
|
48
|
-
:meth
|
|
49
|
+
:meth:`~Cache.getsizeof` returns :const:`1` irrespective of its argument,
|
|
49
50
|
making the cache's size equal to the number of its items, or
|
|
50
51
|
`len(cache)`. For convenience, all cache classes accept an optional
|
|
51
52
|
named constructor parameter `getsizeof`, which may specify a callable
|
|
52
|
-
``getsizeof(value)
|
|
53
|
-
value.
|
|
53
|
+
``getsizeof(value)`` returning a non-negative number as the size of
|
|
54
|
+
an item's value. An item whose size exceeds :attr:`~Cache.maxsize` cannot be
|
|
55
|
+
stored, and trying to do so raises :exc:`ValueError`.
|
|
54
56
|
|
|
55
57
|
The values of a :class:`Cache` are mutable by default, as are e.g. the
|
|
56
58
|
values of a :class:`dict`. It is the user's responsibility to take
|
|
@@ -92,6 +94,9 @@ inserted into the cache.
|
|
|
92
94
|
This class counts how often an item is retrieved, and discards the
|
|
93
95
|
items used least often to make space when necessary.
|
|
94
96
|
|
|
97
|
+
Updating a cache item via :meth:`LFUCache.__setitem__` also counts
|
|
98
|
+
as a use of the item, i.e. it will increment the item's use count.
|
|
99
|
+
|
|
95
100
|
.. autoclass:: LRUCache(maxsize, getsizeof=None)
|
|
96
101
|
:members: popitem
|
|
97
102
|
|
|
@@ -113,8 +118,8 @@ inserted into the cache.
|
|
|
113
118
|
:members: popitem, timer, ttl
|
|
114
119
|
|
|
115
120
|
This class associates a time-to-live value with each item. Items
|
|
116
|
-
that expire because they have exceeded their time-to-live will
|
|
117
|
-
|
|
121
|
+
that expire because they have exceeded their time-to-live will no
|
|
122
|
+
longer be accessible, and will be removed eventually. If no
|
|
118
123
|
expired items are there to remove, the least recently used items
|
|
119
124
|
will be discarded first to make space when necessary.
|
|
120
125
|
|
|
@@ -140,14 +145,13 @@ inserted into the cache.
|
|
|
140
145
|
|
|
141
146
|
.. method:: expire(self, time=None)
|
|
142
147
|
|
|
143
|
-
Expired items will be removed from a cache
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
:attr:`timer`.
|
|
148
|
+
Expired items will be removed from a cache lazily, e.g. when
|
|
149
|
+
:meth:`__setitem__` is next called, and therefore may still
|
|
150
|
+
claim memory. Calling this method removes all items whose
|
|
151
|
+
time-to-live would have expired by `time`, so garbage collection
|
|
152
|
+
is free to reuse their memory. If `time` is :const:`None`, this
|
|
153
|
+
removes all items that have expired by the current value
|
|
154
|
+
returned by :attr:`timer`.
|
|
151
155
|
|
|
152
156
|
:returns: An iterable of expired `(key, value)` pairs.
|
|
153
157
|
|
|
@@ -187,20 +191,19 @@ inserted into the cache.
|
|
|
187
191
|
cache = TLRUCache(maxsize=10, ttu=datetime_ttu, timer=datetime.now)
|
|
188
192
|
|
|
189
193
|
Items that expire because they have exceeded their time-to-use will
|
|
190
|
-
|
|
194
|
+
no longer be accessible, and will be removed eventually. If no
|
|
191
195
|
expired items are there to remove, the least recently used items
|
|
192
196
|
will be discarded first to make space when necessary.
|
|
193
197
|
|
|
194
198
|
.. method:: expire(self, time=None)
|
|
195
199
|
|
|
196
|
-
Expired items will be removed from a cache
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
:attr:`timer`.
|
|
200
|
+
Expired items will be removed from a cache lazily, e.g. when
|
|
201
|
+
:meth:`__setitem__` is next called, and therefore may still
|
|
202
|
+
claim memory. Calling this method removes all items whose
|
|
203
|
+
time-to-use would have expired by `time`, so garbage collection
|
|
204
|
+
is free to reuse their memory. If `time` is :const:`None`, this
|
|
205
|
+
removes all items that have expired by the current value
|
|
206
|
+
returned by :attr:`timer`.
|
|
204
207
|
|
|
205
208
|
:returns: An iterable of expired `(key, value)` pairs.
|
|
206
209
|
|
|
@@ -208,10 +211,11 @@ inserted into the cache.
|
|
|
208
211
|
Extending cache classes
|
|
209
212
|
=======================
|
|
210
213
|
|
|
211
|
-
Sometimes it may be desirable to
|
|
212
|
-
evicted, i.e. removed from a cache to make room for new items
|
|
213
|
-
all cache implementations call :meth:`popitem` to
|
|
214
|
-
cache, this can be achieved by overriding this
|
|
214
|
+
Sometimes it may be desirable to be notified when cache items are
|
|
215
|
+
evicted, i.e. removed from a cache to make room for new items, and
|
|
216
|
+
which ones. Since all cache implementations call :meth:`popitem` to
|
|
217
|
+
evict items from the cache, this can be achieved by overriding this
|
|
218
|
+
method in a subclass:
|
|
215
219
|
|
|
216
220
|
.. doctest::
|
|
217
221
|
|
|
@@ -229,10 +233,10 @@ cache, this can be achieved by overriding this method in a subclass:
|
|
|
229
233
|
|
|
230
234
|
With :class:`TTLCache` and :class:`TLRUCache`, items may also be
|
|
231
235
|
removed after they expire. In this case, :meth:`popitem` will *not*
|
|
232
|
-
be called, but :meth:`expire` will be called from the
|
|
233
|
-
|
|
234
|
-
pairs. By overriding :meth:`expire`, a subclass will
|
|
235
|
-
expired items:
|
|
236
|
+
be called, but :meth:`expire` will be called lazily, e.g. from the
|
|
237
|
+
next :meth:`__setitem__`, and will return an iterable of the expired
|
|
238
|
+
`(key, value)` pairs. By overriding :meth:`expire`, a subclass will
|
|
239
|
+
be able to track expired items:
|
|
236
240
|
|
|
237
241
|
.. doctest::
|
|
238
242
|
|
|
@@ -329,16 +333,16 @@ often called with the same arguments:
|
|
|
329
333
|
therefore must be `thread-safe`_ by itself.
|
|
330
334
|
|
|
331
335
|
If `condition` is not :const:`None`, it must specify a `condition
|
|
332
|
-
variable`_, i.e. an object providing :
|
|
333
|
-
:
|
|
336
|
+
variable`_, i.e. an object providing :meth:`wait()`,
|
|
337
|
+
:meth:`wait_for()`, :meth:`notify()` and :meth:`notify_all()`
|
|
334
338
|
methods as defined by :class:`threading.Condition`. Using a
|
|
335
339
|
`condition` variable will prevent concurrent execution of the
|
|
336
340
|
wrapped function with *identical* parameters, or cache keys.
|
|
337
341
|
Instead, a calling thread will check if an identical function call
|
|
338
|
-
is already executing, and will then
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
+
is already executing, and will then wait for the pending call to
|
|
343
|
+
finish. The executing thread will notify any waiting threads as
|
|
344
|
+
soon as the function completes, which will then return the cached
|
|
345
|
+
function result.
|
|
342
346
|
|
|
343
347
|
.. note::
|
|
344
348
|
|
|
@@ -346,7 +350,7 @@ often called with the same arguments:
|
|
|
346
350
|
make :func:`cached` `thread-safe`_, it may still be subject to
|
|
347
351
|
`cache stampede`_ issues under high load, depending on your
|
|
348
352
|
actual use case. Providing a `condition` variable will mitigate
|
|
349
|
-
these situations, but will
|
|
353
|
+
these situations, but will incur a performance penalty.
|
|
350
354
|
|
|
351
355
|
If no separate `lock` parameter is provided, `condition` must also
|
|
352
356
|
implement the `context manager`_ protocol, and will also be used to
|
|
@@ -374,7 +378,7 @@ often called with the same arguments:
|
|
|
374
378
|
get_pep.cache.pop(key, None)
|
|
375
379
|
|
|
376
380
|
For the common use case of clearing or invalidating the cache, the
|
|
377
|
-
decorator also provides a :
|
|
381
|
+
decorator also provides a :meth:`cache_clear()` function which
|
|
378
382
|
takes care of locking automatically, if needed:
|
|
379
383
|
|
|
380
384
|
.. testcode::
|
|
@@ -389,8 +393,8 @@ often called with the same arguments:
|
|
|
389
393
|
|
|
390
394
|
.. note::
|
|
391
395
|
|
|
392
|
-
|
|
393
|
-
|
|
396
|
+
This will incur a small performance penalty, so it has to be
|
|
397
|
+
enabled explicitly.
|
|
394
398
|
|
|
395
399
|
.. doctest::
|
|
396
400
|
|
|
@@ -427,18 +431,18 @@ often called with the same arguments:
|
|
|
427
431
|
>>> # compute Fibonacci numbers
|
|
428
432
|
>>> @cached(numcache, key=partial(hashkey, 'fib'))
|
|
429
433
|
... def fib(n):
|
|
430
|
-
...
|
|
434
|
+
... return n if n < 2 else fib(n - 1) + fib(n - 2)
|
|
431
435
|
|
|
432
436
|
>>> # compute Lucas numbers
|
|
433
437
|
>>> @cached(numcache, key=partial(hashkey, 'luc'))
|
|
434
438
|
... def luc(n):
|
|
435
|
-
...
|
|
439
|
+
... return 2 - n if n < 2 else luc(n - 1) + luc(n - 2)
|
|
436
440
|
|
|
437
441
|
>>> fib(42)
|
|
438
442
|
267914296
|
|
439
443
|
>>> luc(42)
|
|
440
444
|
599074578
|
|
441
|
-
>>>
|
|
445
|
+
>>> sorted(numcache.items())
|
|
442
446
|
[..., (('fib', 42), 267914296), ..., (('luc', 42), 599074578)]
|
|
443
447
|
|
|
444
448
|
|
|
@@ -507,7 +511,7 @@ often called with the same arguments:
|
|
|
507
511
|
.. doctest::
|
|
508
512
|
|
|
509
513
|
>>> def foo(a=1):
|
|
510
|
-
... _foo(a)
|
|
514
|
+
... return _foo(a)
|
|
511
515
|
...
|
|
512
516
|
>>> @cached(LRUCache(maxsize=100))
|
|
513
517
|
... def _foo(a):
|
|
@@ -533,10 +537,10 @@ often called with the same arguments:
|
|
|
533
537
|
saves results in a cache.
|
|
534
538
|
|
|
535
539
|
The main difference between this and the :func:`cached` function
|
|
536
|
-
decorator is that `cache`, `lock` and `condition` are not
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
+
decorator is that `cache`, `lock` and `condition` are not objects
|
|
541
|
+
but functions. These will be called with `self` as their sole
|
|
542
|
+
argument to retrieve the cache, lock and condition objects for the
|
|
543
|
+
respective instance.
|
|
540
544
|
|
|
541
545
|
.. note::
|
|
542
546
|
|
|
@@ -549,15 +553,15 @@ often called with the same arguments:
|
|
|
549
553
|
`key`, `lock` and `condition` parameters are also available as
|
|
550
554
|
:attr:`cache`, :attr:`cache_key`, :attr:`cache_lock` and
|
|
551
555
|
:attr:`cache_condition` attributes of the wrapped instance method.
|
|
552
|
-
A :
|
|
553
|
-
an optional :
|
|
556
|
+
A :meth:`cache_clear()` function will also be provided, as well as
|
|
557
|
+
an optional :meth:`cache_info()` function reporting per-instance
|
|
554
558
|
cache statistics.
|
|
555
559
|
|
|
556
560
|
Also, mostly for efficiency, this decorator requires that the
|
|
557
561
|
:attr:`__dict__` attribute on each instance be a mutable mapping.
|
|
558
|
-
This means it will not work with some types, such as
|
|
559
|
-
|
|
560
|
-
|
|
562
|
+
This means it will not work with some types, such as those that
|
|
563
|
+
specify :attr:`__slots__` without including :attr:`__dict__` as one
|
|
564
|
+
of the defined slots.
|
|
561
565
|
|
|
562
566
|
One advantage of :func:`cachedmethod` over the :func:`cached`
|
|
563
567
|
function decorator is that cache properties such as `maxsize` can
|
|
@@ -587,7 +591,7 @@ often called with the same arguments:
|
|
|
587
591
|
>>> # remove an individual cached PEP from the cache
|
|
588
592
|
>>> key = peps.get.cache_key(320)
|
|
589
593
|
>>> with peps.get.cache_lock:
|
|
590
|
-
...
|
|
594
|
+
... del peps.get.cache[key]
|
|
591
595
|
|
|
592
596
|
>>> peps.get.cache_info()
|
|
593
597
|
CacheInfo(hits=3, misses=8, maxsize=32, currsize=7)
|
|
@@ -601,13 +605,12 @@ often called with the same arguments:
|
|
|
601
605
|
The `key` function will be called as `key(self, *args, **kwargs)`
|
|
602
606
|
to retrieve a suitable cache key. Note that the default `key`
|
|
603
607
|
function, :func:`cachetools.keys.methodkey`, ignores its first
|
|
604
|
-
implicit argument, i.e.
|
|
605
|
-
reasons, but also ensures that
|
|
606
|
-
hashable.
|
|
608
|
+
implicit argument, i.e. `self`. This is mostly for historical
|
|
609
|
+
reasons, but also ensures that `self` does not have to be hashable.
|
|
607
610
|
|
|
608
611
|
You may provide a different `key` function,
|
|
609
|
-
e.g. :func:`cachetools.keys.hashkey`, if you need
|
|
610
|
-
|
|
612
|
+
e.g. :func:`cachetools.keys.hashkey`, if you need `self` to be part
|
|
613
|
+
of the cache key.
|
|
611
614
|
|
|
612
615
|
When using a shared cache for multiple methods, be aware that
|
|
613
616
|
different cache keys must be created for each method even when
|
|
@@ -634,7 +637,7 @@ often called with the same arguments:
|
|
|
634
637
|
@cachedmethod(lambda self: self.cache, key=partial(methodkey, method='rfc'))
|
|
635
638
|
def get_rfc(self, num):
|
|
636
639
|
"""Retrieve text of an IETF Request for Comments"""
|
|
637
|
-
url = 'https://
|
|
640
|
+
url = 'https://www.rfc-editor.org/rfc/rfc%d.txt' % num
|
|
638
641
|
with urllib.request.urlopen(url) as s:
|
|
639
642
|
return s.read()
|
|
640
643
|
|
|
@@ -652,7 +655,7 @@ often called with the same arguments:
|
|
|
652
655
|
|
|
653
656
|
Note how keyword arguments are used with :func:`functools.partial`
|
|
654
657
|
to create distinct cache keys, to avoid issues with
|
|
655
|
-
:func
|
|
658
|
+
:func:`~cachetools.keys.methodkey` skipping its initial `self` argument.
|
|
656
659
|
|
|
657
660
|
.. versionchanged:: 7.0
|
|
658
661
|
|
|
@@ -664,7 +667,7 @@ often called with the same arguments:
|
|
|
664
667
|
instance methods, and are finally officially supported.
|
|
665
668
|
|
|
666
669
|
Require :attr:`__dict__` to be a mutable mapping to support
|
|
667
|
-
per-instance :
|
|
670
|
+
per-instance :meth:`cache_info()`.
|
|
668
671
|
|
|
669
672
|
Returning :const:`None` from `cache(self)` is no longer supported.
|
|
670
673
|
|
|
@@ -692,18 +695,19 @@ often called with the same arguments:
|
|
|
692
695
|
.. module:: cachetools.keys
|
|
693
696
|
|
|
694
697
|
This module provides several functions that can be used as key
|
|
695
|
-
functions with the :func
|
|
698
|
+
functions with the :func:`~cachetools.cached` and
|
|
699
|
+
:func:`~cachetools.cachedmethod` decorators:
|
|
696
700
|
|
|
697
701
|
.. autofunction:: hashkey
|
|
698
702
|
|
|
699
703
|
This function returns a :class:`tuple` instance suitable as a cache
|
|
700
|
-
key, provided the positional and
|
|
704
|
+
key, provided the positional and keyword arguments are hashable.
|
|
701
705
|
|
|
702
706
|
.. autofunction:: methodkey
|
|
703
707
|
|
|
704
708
|
This function is similar to :func:`hashkey`, but ignores its
|
|
705
709
|
first positional argument, i.e. `self` when used with the
|
|
706
|
-
:func
|
|
710
|
+
:func:`~cachetools.cachedmethod` decorator.
|
|
707
711
|
|
|
708
712
|
.. autofunction:: typedkey
|
|
709
713
|
|
|
@@ -715,12 +719,12 @@ functions with the :func:`cached` and :func:`cachedmethod` decorators:
|
|
|
715
719
|
|
|
716
720
|
This function is similar to :func:`typedkey`, but ignores its
|
|
717
721
|
first positional argument, i.e. `self` when used with the
|
|
718
|
-
:func
|
|
722
|
+
:func:`~cachetools.cachedmethod` decorator.
|
|
719
723
|
|
|
720
724
|
These functions can also be helpful when implementing custom key
|
|
721
725
|
functions for handling some non-hashable arguments. For example,
|
|
722
726
|
calling the following function with a dictionary as its `env` argument
|
|
723
|
-
will raise a :
|
|
727
|
+
will raise a :exc:`TypeError`, since :class:`dict` is not hashable::
|
|
724
728
|
|
|
725
729
|
@cached(LRUCache(maxsize=128))
|
|
726
730
|
def foo(x, y, z, env={}):
|
|
@@ -735,7 +739,7 @@ specially::
|
|
|
735
739
|
key += tuple(sorted(env.items()))
|
|
736
740
|
return key
|
|
737
741
|
|
|
738
|
-
The
|
|
742
|
+
The ``envkey`` function can then be used in decorator declarations
|
|
739
743
|
like this::
|
|
740
744
|
|
|
741
745
|
@cached(LRUCache(maxsize=128), key=envkey)
|
|
@@ -746,19 +750,19 @@ like this::
|
|
|
746
750
|
|
|
747
751
|
|
|
748
752
|
****************************************************************************
|
|
749
|
-
:mod:`cachetools.func` --- :func:`functools.lru_cache
|
|
753
|
+
:mod:`cachetools.func` --- :func:`functools.lru_cache`-compatible decorators
|
|
750
754
|
****************************************************************************
|
|
751
755
|
|
|
752
756
|
.. module:: cachetools.func
|
|
753
757
|
|
|
754
|
-
To ease migration from (or to)
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
758
|
+
To ease migration from (or to) :func:`functools.lru_cache`, this
|
|
759
|
+
module provides several memoizing function decorators with a similar
|
|
760
|
+
API. All these decorators wrap a function with a memoizing callable
|
|
761
|
+
that saves up to `maxsize` results, using different caching
|
|
762
|
+
strategies. If `maxsize` is set to :const:`None`, the size limit is
|
|
763
|
+
disabled and the cache can grow without bound. For :func:`ttl_cache`,
|
|
764
|
+
the time-to-live still applies, so cached results expire after `ttl`
|
|
765
|
+
even without a size limit.
|
|
762
766
|
|
|
763
767
|
If the optional argument `typed` is set to :const:`True`, function
|
|
764
768
|
arguments of different types will be cached separately. For example,
|
|
@@ -774,16 +778,16 @@ leaving the `maxsize` at its default value of 128::
|
|
|
774
778
|
sentence = sentence.casefold()
|
|
775
779
|
return sum(sentence.count(vowel) for vowel in 'aeiou')
|
|
776
780
|
|
|
777
|
-
The wrapped function is instrumented with a :
|
|
781
|
+
The wrapped function is instrumented with a :meth:`cache_parameters`
|
|
778
782
|
function that returns a new :class:`dict` showing the values for
|
|
779
783
|
`maxsize` and `typed`. This is for information purposes only.
|
|
780
784
|
Mutating the values has no effect.
|
|
781
785
|
|
|
782
|
-
The wrapped function is also instrumented with :
|
|
783
|
-
:
|
|
786
|
+
The wrapped function is also instrumented with :meth:`cache_info` and
|
|
787
|
+
:meth:`cache_clear` functions to provide information about cache
|
|
784
788
|
performance and clear the cache. Please see the
|
|
785
789
|
:func:`functools.lru_cache` documentation for details. Also note that
|
|
786
|
-
all the decorators in this module are thread-safe
|
|
790
|
+
all the decorators in this module are thread-safe.
|
|
787
791
|
|
|
788
792
|
|
|
789
793
|
.. decorator:: fifo_cache(user_function)
|
|
@@ -817,8 +821,8 @@ all the decorators in this module are thread-safe by default.
|
|
|
817
821
|
.. decorator:: ttl_cache(user_function)
|
|
818
822
|
ttl_cache(maxsize=128, ttl=600, timer=time.monotonic, typed=False)
|
|
819
823
|
|
|
820
|
-
Decorator
|
|
821
|
-
up to `maxsize` results based on a Least Recently Used (LRU)
|
|
824
|
+
Decorator that wraps a function with a memoizing callable that
|
|
825
|
+
saves up to `maxsize` results based on a Least Recently Used (LRU)
|
|
822
826
|
algorithm with a per-item time-to-live (TTL) value. By default,
|
|
823
827
|
the time-to-live is set to 600 seconds and :func:`time.monotonic`
|
|
824
828
|
is used to retrieve the current time.
|
|
@@ -12,7 +12,7 @@ __all__ = (
|
|
|
12
12
|
"cachedmethod",
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
__version__ = "7.2.
|
|
15
|
+
__version__ = "7.2.5"
|
|
16
16
|
|
|
17
17
|
import collections
|
|
18
18
|
import collections.abc
|
|
@@ -266,7 +266,7 @@ class LFUCache(Cache):
|
|
|
266
266
|
self.__links.clear()
|
|
267
267
|
|
|
268
268
|
def __touch(self, key):
|
|
269
|
-
"""Increment use count"""
|
|
269
|
+
"""Increment use count."""
|
|
270
270
|
link = self.__links[key]
|
|
271
271
|
curr = link.next
|
|
272
272
|
if curr.count != link.count + 1:
|
|
@@ -319,7 +319,7 @@ class LRUCache(Cache):
|
|
|
319
319
|
self.__order.clear()
|
|
320
320
|
|
|
321
321
|
def __touch(self, key):
|
|
322
|
-
"""Mark as recently used"""
|
|
322
|
+
"""Mark as recently used."""
|
|
323
323
|
try:
|
|
324
324
|
self.__order.move_to_end(key)
|
|
325
325
|
except KeyError:
|
|
@@ -370,7 +370,7 @@ class RRCache(Cache):
|
|
|
370
370
|
|
|
371
371
|
|
|
372
372
|
class _TimedCache(Cache):
|
|
373
|
-
"""Base class for time
|
|
373
|
+
"""Base class for time-aware cache implementations."""
|
|
374
374
|
|
|
375
375
|
class _Timer:
|
|
376
376
|
def __init__(self, timer):
|
|
@@ -590,7 +590,7 @@ class TTLCache(_TimedCache):
|
|
|
590
590
|
|
|
591
591
|
|
|
592
592
|
class TLRUCache(_TimedCache):
|
|
593
|
-
"""Time
|
|
593
|
+
"""Time-aware Least Recently Used (TLRU) cache implementation."""
|
|
594
594
|
|
|
595
595
|
__HEAP_CLEANUP_FACTOR = 2 # clean up the heap if size > N * len(items)
|
|
596
596
|
|
|
@@ -668,7 +668,7 @@ class TLRUCache(_TimedCache):
|
|
|
668
668
|
|
|
669
669
|
@property
|
|
670
670
|
def ttu(self):
|
|
671
|
-
"""The
|
|
671
|
+
"""The time-to-use function used by the cache."""
|
|
672
672
|
return self.__ttu
|
|
673
673
|
|
|
674
674
|
def expire(self, time=None):
|
|
@@ -232,6 +232,14 @@ class _cachedmethod_unbound(Generic[_P, _R]):
|
|
|
232
232
|
__qualname__: str
|
|
233
233
|
__doc__: str | None
|
|
234
234
|
@overload
|
|
235
|
+
def __get__(
|
|
236
|
+
self, obj: None, objtype: type | None = None
|
|
237
|
+
) -> _cachedmethod_unbound[_P, _R]: ...
|
|
238
|
+
@overload
|
|
239
|
+
def __get__(
|
|
240
|
+
self, obj: Any, objtype: type | None = None
|
|
241
|
+
) -> _cachedmethod_wrapper[_P, _R]: ...
|
|
242
|
+
@overload
|
|
235
243
|
def __call__(self, obj: Any, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
|
|
236
244
|
# deprecated @classmethod use
|
|
237
245
|
@overload
|
|
@@ -17,16 +17,24 @@ def _none(_):
|
|
|
17
17
|
class _WrapperBase:
|
|
18
18
|
"""Wrapper base class providing default implementations for properties."""
|
|
19
19
|
|
|
20
|
-
def __init__(self, obj, method, cache, key, lock=None, cond=None):
|
|
20
|
+
def __init__(self, obj, descriptor, method, cache, key, lock=None, cond=None):
|
|
21
21
|
if isinstance(obj, type):
|
|
22
22
|
_warn_classmethod(stacklevel=5)
|
|
23
23
|
functools.update_wrapper(self, method)
|
|
24
24
|
self._obj = obj # protected
|
|
25
|
+
self.__descriptor = descriptor
|
|
25
26
|
self.__cache = cache
|
|
26
27
|
self.__key = functools.partial(key, obj)
|
|
27
28
|
self.__lock = lock if lock is not None else _none
|
|
28
29
|
self.__cond = cond if cond is not None else _none
|
|
29
30
|
|
|
31
|
+
def __reduce__(self):
|
|
32
|
+
# rebuild via the descriptor instead of pickling unpicklable
|
|
33
|
+
# internals, e.g. __wrapped__, lock/cond closures; note that
|
|
34
|
+
# this will also reset hits and misses counts for *Info*
|
|
35
|
+
# wrappers
|
|
36
|
+
return (self.__descriptor.__get__, (self._obj,))
|
|
37
|
+
|
|
30
38
|
def __call__(self, *args, **kwargs):
|
|
31
39
|
raise NotImplementedError() # pragma: no cover
|
|
32
40
|
|
|
@@ -59,8 +67,8 @@ class _WrapperBase:
|
|
|
59
67
|
def _condition_info(method, cache, key, lock, cond, info):
|
|
60
68
|
class Descriptor(_MethodDescriptor):
|
|
61
69
|
class Wrapper(_WrapperBase):
|
|
62
|
-
def __init__(self, obj):
|
|
63
|
-
super().__init__(obj, method, cache, key, lock, cond)
|
|
70
|
+
def __init__(self, obj, descriptor):
|
|
71
|
+
super().__init__(obj, descriptor, method, cache, key, lock, cond)
|
|
64
72
|
self.__hits = self.__misses = 0
|
|
65
73
|
self.__pending = set()
|
|
66
74
|
|
|
@@ -107,8 +115,8 @@ def _condition_info(method, cache, key, lock, cond, info):
|
|
|
107
115
|
def _locked_info(method, cache, key, lock, info):
|
|
108
116
|
class Descriptor(_MethodDescriptor):
|
|
109
117
|
class Wrapper(_WrapperBase):
|
|
110
|
-
def __init__(self, obj):
|
|
111
|
-
super().__init__(obj, method, cache, key, lock)
|
|
118
|
+
def __init__(self, obj, descriptor):
|
|
119
|
+
super().__init__(obj, descriptor, method, cache, key, lock)
|
|
112
120
|
self.__hits = self.__misses = 0
|
|
113
121
|
|
|
114
122
|
def __call__(self, *args, **kwargs):
|
|
@@ -147,8 +155,8 @@ def _locked_info(method, cache, key, lock, info):
|
|
|
147
155
|
def _unlocked_info(method, cache, key, info):
|
|
148
156
|
class Descriptor(_MethodDescriptor):
|
|
149
157
|
class Wrapper(_WrapperBase):
|
|
150
|
-
def __init__(self, obj):
|
|
151
|
-
super().__init__(obj, method, cache, key)
|
|
158
|
+
def __init__(self, obj, descriptor):
|
|
159
|
+
super().__init__(obj, descriptor, method, cache, key)
|
|
152
160
|
self.__hits = self.__misses = 0
|
|
153
161
|
|
|
154
162
|
def __call__(self, *args, **kwargs):
|
|
@@ -181,7 +189,7 @@ def _condition(method, cache, key, lock, cond):
|
|
|
181
189
|
# backward-compatible weakref dictionary for Python >= 3.13
|
|
182
190
|
pending = weakref.WeakKeyDictionary()
|
|
183
191
|
|
|
184
|
-
def wrapper(self, pending, *args, **kwargs):
|
|
192
|
+
def wrapper(self, pending, /, *args, **kwargs):
|
|
185
193
|
c = cache(self)
|
|
186
194
|
k = key(self, *args, **kwargs)
|
|
187
195
|
with lock(self):
|
|
@@ -214,8 +222,8 @@ def _condition(method, cache, key, lock, cond):
|
|
|
214
222
|
|
|
215
223
|
class Descriptor(_ClassMethodDescriptor):
|
|
216
224
|
class Wrapper(_WrapperBase):
|
|
217
|
-
def __init__(self, obj):
|
|
218
|
-
super().__init__(obj, method, cache, key, lock, cond)
|
|
225
|
+
def __init__(self, obj, descriptor):
|
|
226
|
+
super().__init__(obj, descriptor, method, cache, key, lock, cond)
|
|
219
227
|
self.__pending = set()
|
|
220
228
|
|
|
221
229
|
def __call__(self, *args, **kwargs):
|
|
@@ -254,8 +262,8 @@ def _locked(method, cache, key, lock):
|
|
|
254
262
|
|
|
255
263
|
class Descriptor(_ClassMethodDescriptor):
|
|
256
264
|
class Wrapper(_WrapperBase):
|
|
257
|
-
def __init__(self, obj):
|
|
258
|
-
super().__init__(obj, method, cache, key, lock)
|
|
265
|
+
def __init__(self, obj, descriptor):
|
|
266
|
+
super().__init__(obj, descriptor, method, cache, key, lock)
|
|
259
267
|
|
|
260
268
|
def __call__(self, *args, **kwargs):
|
|
261
269
|
return wrapper(self._obj, *args, **kwargs)
|
|
@@ -288,8 +296,8 @@ def _unlocked(method, cache, key):
|
|
|
288
296
|
|
|
289
297
|
class Descriptor(_ClassMethodDescriptor):
|
|
290
298
|
class Wrapper(_WrapperBase):
|
|
291
|
-
def __init__(self, obj):
|
|
292
|
-
super().__init__(obj, method, cache, key)
|
|
299
|
+
def __init__(self, obj, descriptor):
|
|
300
|
+
super().__init__(obj, descriptor, method, cache, key)
|
|
293
301
|
|
|
294
302
|
def __call__(self, *args, **kwargs):
|
|
295
303
|
return wrapper(self._obj, *args, **kwargs)
|
|
@@ -26,12 +26,14 @@ class _MethodDescriptor:
|
|
|
26
26
|
"""Descriptor class implementing the basic descriptor protocol."""
|
|
27
27
|
|
|
28
28
|
def __init__(self, deprecated=False):
|
|
29
|
+
self.__owner = None
|
|
29
30
|
self.__attrname = None
|
|
30
31
|
self.__deprecated = deprecated
|
|
31
32
|
self.__unbound = None
|
|
32
33
|
|
|
33
34
|
def __set_name__(self, owner, name):
|
|
34
35
|
if self.__attrname is None:
|
|
36
|
+
self.__owner = owner
|
|
35
37
|
self.__attrname = name
|
|
36
38
|
elif name != self.__attrname:
|
|
37
39
|
raise TypeError(
|
|
@@ -45,7 +47,7 @@ class _MethodDescriptor:
|
|
|
45
47
|
return self.__get_unbound()
|
|
46
48
|
elif self.__attrname is not None:
|
|
47
49
|
# replace descriptor instance with wrapper in instance dict
|
|
48
|
-
wrapper = self.Wrapper(obj) # type: ignore
|
|
50
|
+
wrapper = self.Wrapper(obj, self) # type: ignore
|
|
49
51
|
try:
|
|
50
52
|
descriptor = next(
|
|
51
53
|
(
|
|
@@ -86,12 +88,21 @@ class _MethodDescriptor:
|
|
|
86
88
|
raise TypeError(msg) from None
|
|
87
89
|
elif self.__deprecated:
|
|
88
90
|
# deprecated @classmethod, warning already raised elsewhere
|
|
89
|
-
wrapper = self.Wrapper(obj) # type: ignore
|
|
91
|
+
wrapper = self.Wrapper(obj, self) # type: ignore
|
|
90
92
|
else:
|
|
91
93
|
msg = "Cannot use @cachedmethod instance without calling __set_name__ on it"
|
|
92
94
|
raise TypeError(msg) from None
|
|
93
95
|
return wrapper
|
|
94
96
|
|
|
97
|
+
def __reduce__(self):
|
|
98
|
+
# pickle and copy by reference, so wrappers and their keys in
|
|
99
|
+
# "__cachetools_cachedmethods" can be pickled and deep-copied
|
|
100
|
+
return (self._lookup, (self.__owner, self.__attrname))
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _lookup(owner, name):
|
|
104
|
+
return owner.__dict__[name]
|
|
105
|
+
|
|
95
106
|
def __get_unbound(self):
|
|
96
107
|
# Created once and shared by all class accesses; objtype is not
|
|
97
108
|
# used for instance lookups, so it need not be bound here.
|
|
@@ -111,16 +122,46 @@ class _Unbound:
|
|
|
111
122
|
def __init__(self, descriptor):
|
|
112
123
|
functools.update_wrapper(self, descriptor.__wrapped__)
|
|
113
124
|
self.__descriptor = descriptor
|
|
125
|
+
self.__receiver = None # name of the wrapped method's first parameter
|
|
126
|
+
|
|
127
|
+
def __reduce__(self):
|
|
128
|
+
# pickle and copy by reference, as functions are
|
|
129
|
+
return (self.__descriptor.__get__, (None,))
|
|
130
|
+
|
|
131
|
+
# bind like a method when aliased in another class body
|
|
132
|
+
def __get__(self, obj, objtype=None):
|
|
133
|
+
if obj is None:
|
|
134
|
+
return self
|
|
135
|
+
return self.__descriptor.__get__(obj)
|
|
114
136
|
|
|
115
137
|
# Receiver is taken from *args so autospec'd instances accept any call,
|
|
116
138
|
# as they did before class access returned a callable.
|
|
117
139
|
def __call__(self, /, *args, **kwargs):
|
|
118
|
-
if
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
140
|
+
if args:
|
|
141
|
+
obj, *args = args
|
|
142
|
+
else:
|
|
143
|
+
obj = self.__pop_receiver(kwargs)
|
|
122
144
|
return self.__descriptor.__get__(obj)(*args, **kwargs)
|
|
123
145
|
|
|
146
|
+
def __pop_receiver(self, kwargs):
|
|
147
|
+
# the receiver may also be passed by keyword, as for a plain function
|
|
148
|
+
if self.__receiver is None:
|
|
149
|
+
import inspect
|
|
150
|
+
|
|
151
|
+
self.__receiver = ""
|
|
152
|
+
try:
|
|
153
|
+
param = next(iter(inspect.signature(self).parameters.values()))
|
|
154
|
+
except (ValueError, StopIteration):
|
|
155
|
+
pass # no signature, or no parameters at all
|
|
156
|
+
else:
|
|
157
|
+
if param.kind is param.POSITIONAL_OR_KEYWORD:
|
|
158
|
+
self.__receiver = param.name
|
|
159
|
+
try:
|
|
160
|
+
return kwargs.pop(self.__receiver)
|
|
161
|
+
except KeyError:
|
|
162
|
+
msg = "Cannot call @cachedmethod through the class without an instance"
|
|
163
|
+
raise TypeError(msg) from None
|
|
164
|
+
|
|
124
165
|
|
|
125
166
|
class _ClassMethodDescriptor(_MethodDescriptor):
|
|
126
167
|
"""Descriptor base class supporting deprecated @classmethod use."""
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
import warnings
|
|
3
|
+
from collections.abc import Callable
|
|
3
4
|
|
|
4
5
|
from cachetools import Cache, cachedmethod, keys
|
|
5
6
|
|
|
@@ -69,6 +70,18 @@ class Cached:
|
|
|
69
70
|
def get_lock_cond_info(self, value):
|
|
70
71
|
return self.__get(value)
|
|
71
72
|
|
|
73
|
+
get_aliased = cachedmethod(lambda self: self.cache)(__get)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CachedChild(Cached):
|
|
77
|
+
def __init__(self, cache, child_cache):
|
|
78
|
+
super().__init__(cache)
|
|
79
|
+
self.child_cache = child_cache
|
|
80
|
+
|
|
81
|
+
@cachedmethod(lambda self: self.child_cache, info=True)
|
|
82
|
+
def get_info(self, value):
|
|
83
|
+
return super().get_info(value) + 1
|
|
84
|
+
|
|
72
85
|
|
|
73
86
|
class Unhashable(Cached):
|
|
74
87
|
# https://github.com/tkem/cachetools/issues/107
|
|
@@ -421,6 +434,121 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
|
|
|
421
434
|
self.assertEqual(len(cache), 0)
|
|
422
435
|
self.assertEqual(cached.cond.count, 4)
|
|
423
436
|
|
|
437
|
+
def test_decorator_pickle(self):
|
|
438
|
+
import pickle
|
|
439
|
+
|
|
440
|
+
cache = self.cache(3)
|
|
441
|
+
cached = Cached(cache)
|
|
442
|
+
self.assertEqual(len(cache), 0)
|
|
443
|
+
|
|
444
|
+
unpickled = pickle.loads(pickle.dumps(cached))
|
|
445
|
+
self.assertEqual(len(unpickled.get_cond.cache), 0)
|
|
446
|
+
self.assertEqual(len(cache), 0)
|
|
447
|
+
self.assertEqual(unpickled.get_cond(0), 0)
|
|
448
|
+
self.assertEqual(len(unpickled.get_cond.cache), 1)
|
|
449
|
+
self.assertEqual(len(cache), 0)
|
|
450
|
+
self.assertEqual(unpickled.get_cond(1), 1)
|
|
451
|
+
self.assertEqual(len(unpickled.get_cond.cache), 2)
|
|
452
|
+
self.assertEqual(len(cache), 0)
|
|
453
|
+
|
|
454
|
+
self.assertEqual(len(cache), 0)
|
|
455
|
+
self.assertEqual(cached.get_cond(0), 0)
|
|
456
|
+
self.assertEqual(len(cache), 1)
|
|
457
|
+
|
|
458
|
+
unpickled = pickle.loads(pickle.dumps(cached))
|
|
459
|
+
self.assertEqual(len(unpickled.get_cond.cache), 1)
|
|
460
|
+
self.assertEqual(len(cache), 1)
|
|
461
|
+
self.assertEqual(unpickled.get_cond(0), 0)
|
|
462
|
+
self.assertEqual(len(unpickled.get_cond.cache), 1)
|
|
463
|
+
self.assertEqual(len(cache), 1)
|
|
464
|
+
self.assertEqual(unpickled.get_cond(1), 1)
|
|
465
|
+
self.assertEqual(len(unpickled.get_cond.cache), 2)
|
|
466
|
+
self.assertEqual(len(cache), 1)
|
|
467
|
+
|
|
468
|
+
def test_decorator_pickle_info(self):
|
|
469
|
+
import pickle
|
|
470
|
+
|
|
471
|
+
cache = self.cache(3)
|
|
472
|
+
cached = Cached(cache)
|
|
473
|
+
maxsize = cache.maxsize if isinstance(cache, Cache) else None
|
|
474
|
+
self.assertEqual(len(cache), 0)
|
|
475
|
+
|
|
476
|
+
# hits and misses will be reset when unpickling - since these
|
|
477
|
+
# are primarily used for debugging/tuning, this is probably OK
|
|
478
|
+
unpickled = pickle.loads(pickle.dumps(cached))
|
|
479
|
+
self.assertEqual(unpickled.get_cond_info.cache_info(), (0, 0, maxsize, 0))
|
|
480
|
+
self.assertEqual(unpickled.get_cond_info(0), 0)
|
|
481
|
+
self.assertEqual(unpickled.get_cond_info.cache_info(), (0, 1, maxsize, 1))
|
|
482
|
+
self.assertEqual(unpickled.get_cond_info(1), 1)
|
|
483
|
+
self.assertEqual(unpickled.get_cond_info.cache_info(), (0, 2, maxsize, 2))
|
|
484
|
+
self.assertEqual(len(cache), 0)
|
|
485
|
+
|
|
486
|
+
self.assertEqual(cached.get_cond_info(0), 0)
|
|
487
|
+
self.assertEqual(len(cache), 1)
|
|
488
|
+
|
|
489
|
+
unpickled = pickle.loads(pickle.dumps(cached))
|
|
490
|
+
self.assertEqual(unpickled.get_cond_info.cache_info(), (0, 0, maxsize, 1))
|
|
491
|
+
self.assertEqual(unpickled.get_cond_info(0), 0)
|
|
492
|
+
self.assertEqual(unpickled.get_cond_info.cache_info(), (1, 0, maxsize, 1))
|
|
493
|
+
self.assertEqual(unpickled.get_cond_info(1), 1)
|
|
494
|
+
self.assertEqual(unpickled.get_cond_info.cache_info(), (1, 1, maxsize, 2))
|
|
495
|
+
self.assertEqual(unpickled.get_cond_info(0), 0)
|
|
496
|
+
self.assertEqual(unpickled.get_cond_info.cache_info(), (2, 1, maxsize, 2))
|
|
497
|
+
self.assertEqual(len(cache), 1)
|
|
498
|
+
|
|
499
|
+
def test_decorator_pickle_super(self):
|
|
500
|
+
import pickle
|
|
501
|
+
|
|
502
|
+
self._test_copy_super(lambda obj: pickle.loads(pickle.dumps(obj)))
|
|
503
|
+
|
|
504
|
+
def test_decorator_deepcopy_super(self):
|
|
505
|
+
import copy
|
|
506
|
+
|
|
507
|
+
self._test_copy_super(copy.deepcopy)
|
|
508
|
+
|
|
509
|
+
def _test_copy_super(self, copyfunc: Callable[[CachedChild], CachedChild]):
|
|
510
|
+
base_cache = self.cache(3)
|
|
511
|
+
maxsize = base_cache.maxsize if isinstance(base_cache, Cache) else None
|
|
512
|
+
cached = CachedChild(base_cache, self.cache(3))
|
|
513
|
+
self.assertEqual(cached.get_info(0), 1)
|
|
514
|
+
|
|
515
|
+
copied = copyfunc(cached)
|
|
516
|
+
child = copied.get_info
|
|
517
|
+
base = super(CachedChild, copied).get_info
|
|
518
|
+
self.assertIsNot(base, child)
|
|
519
|
+
self.assertIs(base, super(CachedChild, copied).get_info)
|
|
520
|
+
self.assertIs(child.cache, copied.child_cache)
|
|
521
|
+
self.assertIs(base.cache, copied.cache)
|
|
522
|
+
self.assertEqual(len(vars(copied)["__cachetools_cachedmethods"]), 1)
|
|
523
|
+
# hits and misses are reset
|
|
524
|
+
self.assertEqual(child.cache_info(), (0, 0, maxsize, 1))
|
|
525
|
+
self.assertEqual(base.cache_info(), (0, 0, maxsize, 1))
|
|
526
|
+
|
|
527
|
+
self.assertEqual(copied.get_info(0), 1)
|
|
528
|
+
self.assertEqual(child.cache_info(), (1, 0, maxsize, 1))
|
|
529
|
+
self.assertEqual(base.cache_info(), (0, 0, maxsize, 1))
|
|
530
|
+
self.assertEqual(copied.get_info(1), 2)
|
|
531
|
+
self.assertEqual(child.cache_info(), (1, 1, maxsize, 2))
|
|
532
|
+
self.assertEqual(base.cache_info(), (0, 1, maxsize, 2))
|
|
533
|
+
self.assertEqual(len(vars(copied)["__cachetools_cachedmethods"]), 1)
|
|
534
|
+
self.assertEqual(len(base_cache), 1)
|
|
535
|
+
|
|
536
|
+
def test_decorator_pickle_aliased(self):
|
|
537
|
+
import pickle
|
|
538
|
+
|
|
539
|
+
cache = self.cache(3)
|
|
540
|
+
cached = Cached(cache)
|
|
541
|
+
self.assertEqual(len(cache), 0)
|
|
542
|
+
|
|
543
|
+
self.assertEqual(cached.get_aliased(0), 0)
|
|
544
|
+
self.assertEqual(len(cache), 1)
|
|
545
|
+
|
|
546
|
+
unpickled = pickle.loads(pickle.dumps(cached))
|
|
547
|
+
self.assertEqual(len(unpickled.get_aliased.cache), 1)
|
|
548
|
+
self.assertEqual(unpickled.get_aliased(0), 0)
|
|
549
|
+
self.assertEqual(len(unpickled.get_aliased.cache), 1)
|
|
550
|
+
self.assertEqual(len(cache), 1)
|
|
551
|
+
|
|
424
552
|
def test_decorator_slots(self):
|
|
425
553
|
|
|
426
554
|
class Slots:
|
|
@@ -706,6 +834,28 @@ class CacheMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
|
|
|
706
834
|
self.assertEqual(super(GrandChild, cached).get_typed(0), 1)
|
|
707
835
|
self.assertEqual(super(Child, cached).get_typed(0.0), 0)
|
|
708
836
|
|
|
837
|
+
def test_cond_pending_keyword(self):
|
|
838
|
+
# Up until cachetools v7.2.4, @cachedmethod with condition and
|
|
839
|
+
# info=False raised a TypeError when the wrapped method
|
|
840
|
+
# received a valid "pending" keyword argument.
|
|
841
|
+
for info in (False, True):
|
|
842
|
+
with self.subTest(info=info):
|
|
843
|
+
|
|
844
|
+
class Pending(Cached):
|
|
845
|
+
@cachedmethod(
|
|
846
|
+
lambda obj: obj.cache,
|
|
847
|
+
condition=lambda obj: obj.cond,
|
|
848
|
+
info=info,
|
|
849
|
+
)
|
|
850
|
+
def get_pending(self, pending):
|
|
851
|
+
return self.get(pending)
|
|
852
|
+
|
|
853
|
+
cached = Pending(self.cache(2))
|
|
854
|
+
self.assertEqual(cached.get_pending(pending=0), 0)
|
|
855
|
+
self.assertEqual(cached.get_pending(pending=0), 0)
|
|
856
|
+
cached.get_pending.cache_clear()
|
|
857
|
+
self.assertEqual(cached.get_pending(pending=0), 1)
|
|
858
|
+
|
|
709
859
|
|
|
710
860
|
class DictMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
|
|
711
861
|
def cache(self, minsize, **_kwargs):
|
|
@@ -791,6 +941,41 @@ class UnboundMethodTest(unittest.TestCase):
|
|
|
791
941
|
with self.assertRaises(TypeError):
|
|
792
942
|
Cached.get() # type: ignore
|
|
793
943
|
|
|
944
|
+
def test_class_call_keyword_receiver(self):
|
|
945
|
+
cached = Cached(Cache(2))
|
|
946
|
+
self.assertEqual(Cached.get(self=cached, value=1), 0) # type: ignore
|
|
947
|
+
self.assertEqual(Cached.get(cached, value=1), 0)
|
|
948
|
+
self.assertEqual(Cached.get(self=cached, value=2), 1) # type: ignore
|
|
949
|
+
self.assertEqual(cached.count, 2)
|
|
950
|
+
with self.assertRaises(TypeError):
|
|
951
|
+
Cached.get(value=1)
|
|
952
|
+
|
|
953
|
+
class Named:
|
|
954
|
+
def __init__(self, cache):
|
|
955
|
+
self.cache = cache
|
|
956
|
+
|
|
957
|
+
@cachedmethod(lambda this: this.cache)
|
|
958
|
+
def get(this, value):
|
|
959
|
+
return value
|
|
960
|
+
|
|
961
|
+
@cachedmethod(lambda self: self.cache)
|
|
962
|
+
def get_posonly(self, /, value):
|
|
963
|
+
return value
|
|
964
|
+
|
|
965
|
+
@cachedmethod(lambda self: self.cache) # type: ignore
|
|
966
|
+
def get_noargs():
|
|
967
|
+
return 42
|
|
968
|
+
|
|
969
|
+
named = Named(Cache(2))
|
|
970
|
+
self.assertEqual(Named.get(this=named, value=1), 1) # type: ignore
|
|
971
|
+
self.assertEqual(Named.get(named, value=1), 1)
|
|
972
|
+
with self.assertRaises(TypeError):
|
|
973
|
+
Named.get(self=named, value=1) # type: ignore
|
|
974
|
+
with self.assertRaises(TypeError):
|
|
975
|
+
Named.get_posonly(self=named, value=1) # type: ignore
|
|
976
|
+
with self.assertRaises(TypeError):
|
|
977
|
+
Named.get_noargs()
|
|
978
|
+
|
|
794
979
|
def test_class_access_identity(self):
|
|
795
980
|
self.assertIs(Cached.get, Cached.get)
|
|
796
981
|
self.assertIs(Cached.get_info, Cached.get_info)
|
|
@@ -806,6 +991,30 @@ class UnboundMethodTest(unittest.TestCase):
|
|
|
806
991
|
self.assertEqual(Cached.get.__wrapped__(cached, 1), 1)
|
|
807
992
|
self.assertEqual(len(cached.cache), 0)
|
|
808
993
|
|
|
994
|
+
def test_class_access_pickle(self):
|
|
995
|
+
import pickle
|
|
996
|
+
|
|
997
|
+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
|
|
998
|
+
with self.subTest(proto=proto):
|
|
999
|
+
self.assertIs(pickle.loads(pickle.dumps(Cached.get, proto)), Cached.get)
|
|
1000
|
+
|
|
1001
|
+
def test_class_access_copy(self):
|
|
1002
|
+
import copy
|
|
1003
|
+
|
|
1004
|
+
self.assertIs(copy.copy(Cached.get), Cached.get)
|
|
1005
|
+
self.assertIs(copy.deepcopy(Cached.get), Cached.get)
|
|
1006
|
+
|
|
1007
|
+
def test_class_access_alias(self):
|
|
1008
|
+
class Alias(Cached):
|
|
1009
|
+
alias = Cached.get
|
|
1010
|
+
|
|
1011
|
+
self.assertIs(Alias.alias, Cached.get)
|
|
1012
|
+
cached = Alias(Cache(2))
|
|
1013
|
+
self.assertEqual(cached.alias(1), 0)
|
|
1014
|
+
self.assertEqual(cached.alias(1), 0)
|
|
1015
|
+
self.assertIs(cached.alias, cached.get)
|
|
1016
|
+
self.assertEqual(len(cached.cache), 1)
|
|
1017
|
+
|
|
809
1018
|
def test_autospec_no_warnings(self):
|
|
810
1019
|
import unittest.mock
|
|
811
1020
|
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|