cachetools 7.2.6__tar.gz → 7.2.8__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.6 → cachetools-7.2.8}/CHANGELOG.rst +17 -0
- {cachetools-7.2.6/src/cachetools.egg-info → cachetools-7.2.8}/PKG-INFO +1 -1
- {cachetools-7.2.6 → cachetools-7.2.8}/docs/index.rst +12 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/__init__.py +58 -60
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/__init__.pyi +7 -9
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/_cachedmethod.py +10 -10
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/_descriptor.py +43 -37
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/keys.py +2 -2
- {cachetools-7.2.6 → cachetools-7.2.8/src/cachetools.egg-info}/PKG-INFO +1 -1
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_cachedmethod.py +73 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_classmethod.py +43 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_keys.py +10 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_threading.py +86 -10
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_tlru.py +20 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_ttl.py +15 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/LICENSE +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/MANIFEST.in +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/README.rst +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/docs/conf.py +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/pyproject.toml +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/setup.cfg +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/_cached.py +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/func.py +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/func.pyi +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/keys.pyi +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/py.typed +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools.egg-info/SOURCES.txt +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools.egg-info/dependency_links.txt +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools.egg-info/top_level.txt +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/__init__.py +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_cache.py +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_cached.py +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_fifo.py +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_func.py +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_lfu.py +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_lru.py +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_rr.py +0 -0
- {cachetools-7.2.6 → cachetools-7.2.8}/tox.ini +0 -0
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
v7.2.8 (2026-10-31)
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
- Fix type stubs for ``TTLCache`` and ``TLRUCache`` with default
|
|
5
|
+
``timer``.
|
|
6
|
+
|
|
7
|
+
- Minor documentation and unit test improvements.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
v7.2.7 (2026-10-28)
|
|
11
|
+
===================
|
|
12
|
+
|
|
13
|
+
- Various performance improvements.
|
|
14
|
+
|
|
15
|
+
- Minor unit test improvements.
|
|
16
|
+
|
|
17
|
+
|
|
1
18
|
v7.2.6 (2026-10-26)
|
|
2
19
|
===================
|
|
3
20
|
|
|
@@ -356,6 +356,16 @@ often called with the same arguments:
|
|
|
356
356
|
implement the `context manager`_ protocol, and will also be used to
|
|
357
357
|
guard access to the cache.
|
|
358
358
|
|
|
359
|
+
.. note::
|
|
360
|
+
|
|
361
|
+
Even though `lock` and `condition` can be passed separately,
|
|
362
|
+
the condition variable must be associated with the lock that
|
|
363
|
+
guards the cache. In practice, when using
|
|
364
|
+
:class:`threading.Lock` and :class:`threading.Condition`, the
|
|
365
|
+
condition variable must be created with the corresponding lock,
|
|
366
|
+
e.g. ``condition=threading.Condition(lock)``; an unrelated
|
|
367
|
+
:class:`threading.Condition` will raise :exc:`RuntimeError`.
|
|
368
|
+
|
|
359
369
|
The decorator's `cache`, `key`, `lock` and `condition` parameters
|
|
360
370
|
are also available as :attr:`cache`, :attr:`cache_key`,
|
|
361
371
|
:attr:`cache_lock` and :attr:`cache_condition` attributes of the
|
|
@@ -548,6 +558,8 @@ often called with the same arguments:
|
|
|
548
558
|
`lock(self)` will only guard access to the cache itself. It is
|
|
549
559
|
the user's responsibility to handle concurrent calls to the
|
|
550
560
|
underlying wrapped method in a multithreaded environment.
|
|
561
|
+
Likewise, a condition variable returned by `condition(self)`
|
|
562
|
+
must be associated with a lock returned by `lock(self)`.
|
|
551
563
|
|
|
552
564
|
Similarly to the :func:`cached` function decorator, the `cache`,
|
|
553
565
|
`key`, `lock` and `condition` parameters are also available as
|
|
@@ -12,7 +12,7 @@ __all__ = (
|
|
|
12
12
|
"cachedmethod",
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
__version__ = "7.2.
|
|
15
|
+
__version__ = "7.2.8"
|
|
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 =
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
|
@@ -36,8 +36,6 @@ _T = TypeVar("_T")
|
|
|
36
36
|
_KT = TypeVar("_KT")
|
|
37
37
|
_VT = TypeVar("_VT")
|
|
38
38
|
_TT = TypeVar("_TT", default=float)
|
|
39
|
-
_KT2 = TypeVar("_KT2")
|
|
40
|
-
_VT2 = TypeVar("_VT2")
|
|
41
39
|
|
|
42
40
|
class Cache(MutableMapping[_KT, _VT]):
|
|
43
41
|
def __init__(
|
|
@@ -94,13 +92,13 @@ class _TimedCache(Cache[_KT, _VT], Generic[_KT, _VT, _TT]):
|
|
|
94
92
|
def timer(self) -> _Timer[_TT]: ...
|
|
95
93
|
|
|
96
94
|
class TTLCache(_TimedCache[_KT, _VT, _TT]):
|
|
97
|
-
@overload
|
|
95
|
+
@overload # without timer, _TT takes its default
|
|
98
96
|
def __init__(
|
|
99
|
-
self
|
|
97
|
+
self,
|
|
100
98
|
maxsize: float,
|
|
101
99
|
ttl: float,
|
|
102
100
|
*,
|
|
103
|
-
getsizeof: Callable[[
|
|
101
|
+
getsizeof: Callable[[_VT], float] | None = None,
|
|
104
102
|
) -> None: ...
|
|
105
103
|
@overload
|
|
106
104
|
def __init__(
|
|
@@ -115,13 +113,13 @@ class TTLCache(_TimedCache[_KT, _VT, _TT]):
|
|
|
115
113
|
def expire(self, time: _TT | None = None) -> list[tuple[_KT, _VT]]: ...
|
|
116
114
|
|
|
117
115
|
class TLRUCache(_TimedCache[_KT, _VT, _TT]):
|
|
118
|
-
@overload
|
|
116
|
+
@overload # without timer, _TT takes its default
|
|
119
117
|
def __init__(
|
|
120
|
-
self
|
|
118
|
+
self,
|
|
121
119
|
maxsize: float,
|
|
122
|
-
ttu: Callable[[
|
|
120
|
+
ttu: Callable[[_KT, _VT, float], float],
|
|
123
121
|
*,
|
|
124
|
-
getsizeof: Callable[[
|
|
122
|
+
getsizeof: Callable[[_VT], float] | None = None,
|
|
125
123
|
) -> None: ...
|
|
126
124
|
@overload
|
|
127
125
|
def __init__(
|
|
@@ -35,7 +35,7 @@ class _WrapperBase:
|
|
|
35
35
|
# wrappers
|
|
36
36
|
return (self.__descriptor.__get__, (self._obj,))
|
|
37
37
|
|
|
38
|
-
def __call__(self, *args, **kwargs):
|
|
38
|
+
def __call__(self, /, *args, **kwargs):
|
|
39
39
|
raise NotImplementedError() # pragma: no cover
|
|
40
40
|
|
|
41
41
|
def cache_clear(self):
|
|
@@ -72,7 +72,7 @@ def _condition_info(method, cache, key, lock, cond, info):
|
|
|
72
72
|
self.__hits = self.__misses = 0
|
|
73
73
|
self.__pending = set()
|
|
74
74
|
|
|
75
|
-
def __call__(self, *args, **kwargs):
|
|
75
|
+
def __call__(self, /, *args, **kwargs):
|
|
76
76
|
cache = self.cache
|
|
77
77
|
lock = self.cache_lock
|
|
78
78
|
cond = self.cache_condition
|
|
@@ -121,7 +121,7 @@ def _locked_info(method, cache, key, lock, info):
|
|
|
121
121
|
super().__init__(obj, descriptor, method, cache, key, lock)
|
|
122
122
|
self.__hits = self.__misses = 0
|
|
123
123
|
|
|
124
|
-
def __call__(self, *args, **kwargs):
|
|
124
|
+
def __call__(self, /, *args, **kwargs):
|
|
125
125
|
cache = self.cache
|
|
126
126
|
lock = self.cache_lock
|
|
127
127
|
key = self.cache_key(*args, **kwargs)
|
|
@@ -161,7 +161,7 @@ def _unlocked_info(method, cache, key, info):
|
|
|
161
161
|
super().__init__(obj, descriptor, method, cache, key)
|
|
162
162
|
self.__hits = self.__misses = 0
|
|
163
163
|
|
|
164
|
-
def __call__(self, *args, **kwargs):
|
|
164
|
+
def __call__(self, /, *args, **kwargs):
|
|
165
165
|
cache = self.cache
|
|
166
166
|
key = self.cache_key(*args, **kwargs)
|
|
167
167
|
try:
|
|
@@ -220,7 +220,7 @@ def _condition(method, cache, key, lock, cond):
|
|
|
220
220
|
with lock(self):
|
|
221
221
|
c.clear()
|
|
222
222
|
|
|
223
|
-
def classmethod_wrapper(self, *args, **kwargs):
|
|
223
|
+
def classmethod_wrapper(self, /, *args, **kwargs):
|
|
224
224
|
p = pending.setdefault(self, set())
|
|
225
225
|
return wrapper(self, p, *args, **kwargs)
|
|
226
226
|
|
|
@@ -230,7 +230,7 @@ def _condition(method, cache, key, lock, cond):
|
|
|
230
230
|
super().__init__(obj, descriptor, method, cache, key, lock, cond)
|
|
231
231
|
self.__pending = set()
|
|
232
232
|
|
|
233
|
-
def __call__(self, *args, **kwargs):
|
|
233
|
+
def __call__(self, /, *args, **kwargs):
|
|
234
234
|
return wrapper(self._obj, self.__pending, *args, **kwargs)
|
|
235
235
|
|
|
236
236
|
# objtype: backward-compatible @classmethod handling with Python < 3.13
|
|
@@ -241,7 +241,7 @@ def _condition(method, cache, key, lock, cond):
|
|
|
241
241
|
|
|
242
242
|
|
|
243
243
|
def _locked(method, cache, key, lock):
|
|
244
|
-
def wrapper(self, *args, **kwargs):
|
|
244
|
+
def wrapper(self, /, *args, **kwargs):
|
|
245
245
|
c = cache(self)
|
|
246
246
|
k = key(self, *args, **kwargs)
|
|
247
247
|
with lock(self):
|
|
@@ -269,7 +269,7 @@ def _locked(method, cache, key, lock):
|
|
|
269
269
|
def __init__(self, obj, descriptor):
|
|
270
270
|
super().__init__(obj, descriptor, method, cache, key, lock)
|
|
271
271
|
|
|
272
|
-
def __call__(self, *args, **kwargs):
|
|
272
|
+
def __call__(self, /, *args, **kwargs):
|
|
273
273
|
return wrapper(self._obj, *args, **kwargs)
|
|
274
274
|
|
|
275
275
|
# objtype: backward-compatible @classmethod handling with Python < 3.13
|
|
@@ -280,7 +280,7 @@ def _locked(method, cache, key, lock):
|
|
|
280
280
|
|
|
281
281
|
|
|
282
282
|
def _unlocked(method, cache, key):
|
|
283
|
-
def wrapper(self, *args, **kwargs):
|
|
283
|
+
def wrapper(self, /, *args, **kwargs):
|
|
284
284
|
c = cache(self)
|
|
285
285
|
k = key(self, *args, **kwargs)
|
|
286
286
|
try:
|
|
@@ -303,7 +303,7 @@ def _unlocked(method, cache, key):
|
|
|
303
303
|
def __init__(self, obj, descriptor):
|
|
304
304
|
super().__init__(obj, descriptor, method, cache, key)
|
|
305
305
|
|
|
306
|
-
def __call__(self, *args, **kwargs):
|
|
306
|
+
def __call__(self, /, *args, **kwargs):
|
|
307
307
|
return wrapper(self._obj, *args, **kwargs)
|
|
308
308
|
|
|
309
309
|
# objtype: backward-compatible @classmethod handling with Python < 3.13
|
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
@@ -54,7 +54,7 @@ def hashkey(*args, **kwargs):
|
|
|
54
54
|
return _HashedTuple(args)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def methodkey(self, *args, **kwargs):
|
|
57
|
+
def methodkey(self, /, *args, **kwargs):
|
|
58
58
|
"""Return a cache key for use with cached methods."""
|
|
59
59
|
return hashkey(*args, **kwargs)
|
|
60
60
|
|
|
@@ -72,6 +72,6 @@ def typedkey(*args, **kwargs):
|
|
|
72
72
|
return key
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
def typedmethodkey(self, *args, **kwargs):
|
|
75
|
+
def typedmethodkey(self, /, *args, **kwargs):
|
|
76
76
|
"""Return a typed cache key for use with cached methods."""
|
|
77
77
|
return typedkey(*args, **kwargs)
|
|
@@ -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
|
|
@@ -38,6 +38,9 @@ class CacheKeysTest(unittest.TestCase):
|
|
|
38
38
|
# untyped keys compare equal
|
|
39
39
|
self.assertEqual(key("x", 1, 2, 3), key("y", 1.0, 2.0, 3.0))
|
|
40
40
|
self.assertEqual(hash(key("x", 1, 2, 3)), hash(key("y", 1.0, 2.0, 3.0)))
|
|
41
|
+
# "self" is positional-only, so it may also be a keyword argument
|
|
42
|
+
self.assertEqual(key("x", self=1), key("y", self=1))
|
|
43
|
+
self.assertNotEqual(key("x", self=1), key("x"))
|
|
41
44
|
|
|
42
45
|
def test_typedkey(self, key=cachetools.keys.typedkey):
|
|
43
46
|
self.assertEqual(key(), key())
|
|
@@ -54,6 +57,8 @@ class CacheKeysTest(unittest.TestCase):
|
|
|
54
57
|
hash(key({}))
|
|
55
58
|
# typed keys compare unequal
|
|
56
59
|
self.assertNotEqual(key(1, 2, 3), key(1.0, 2.0, 3.0))
|
|
60
|
+
self.assertNotEqual(key(1, 2, 3, x=0), key(1, 2, 3, x=0.0))
|
|
61
|
+
self.assertNotEqual(key(1, 2, 3, x=0), key(1, 2, 3, x=False))
|
|
57
62
|
|
|
58
63
|
def test_typedmethodkey(self, key=cachetools.keys.typedmethodkey):
|
|
59
64
|
# similar to typedkey(), but ignores its first positional argument
|
|
@@ -71,6 +76,11 @@ class CacheKeysTest(unittest.TestCase):
|
|
|
71
76
|
hash(key("x", {}))
|
|
72
77
|
# typed keys compare unequal
|
|
73
78
|
self.assertNotEqual(key("x", 1, 2, 3), key("x", 1.0, 2.0, 3.0))
|
|
79
|
+
self.assertNotEqual(key("x", 1, 2, 3, x=0), key("x", 1, 2, 3, x=0.0))
|
|
80
|
+
self.assertNotEqual(key("x", 1, 2, 3, x=0), key("x", 1, 2, 3, x=False))
|
|
81
|
+
# "self" is positional-only, so it may also be a keyword argument
|
|
82
|
+
self.assertEqual(key("x", self=1), key("y", self=1))
|
|
83
|
+
self.assertNotEqual(key("x", self=1), key("x", self=1.0))
|
|
74
84
|
|
|
75
85
|
def test_addkeys(self, key=cachetools.keys.hashkey):
|
|
76
86
|
self.assertIsInstance(key(), tuple)
|
|
@@ -5,8 +5,8 @@ from cachetools import LRUCache, cached, cachedmethod
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class WaitCondition(Condition):
|
|
8
|
-
def __init__(self):
|
|
9
|
-
super().__init__()
|
|
8
|
+
def __init__(self, lock=None):
|
|
9
|
+
super().__init__(lock)
|
|
10
10
|
self.waiting = Event()
|
|
11
11
|
|
|
12
12
|
def wait(self, timeout=None):
|
|
@@ -93,6 +93,90 @@ class ThreadingTest(unittest.TestCase):
|
|
|
93
93
|
func = Cached().method
|
|
94
94
|
self.run_cond_wait_test(func, cond, started, release, calls)
|
|
95
95
|
|
|
96
|
+
def test_cached_lock_cond(self):
|
|
97
|
+
for info in (False, True):
|
|
98
|
+
with self.subTest(info=info):
|
|
99
|
+
cache = LRUCache(1)
|
|
100
|
+
lock = Lock()
|
|
101
|
+
cond = WaitCondition(lock)
|
|
102
|
+
started = Event()
|
|
103
|
+
release = Event()
|
|
104
|
+
calls = []
|
|
105
|
+
|
|
106
|
+
@cached(cache, lock=lock, condition=cond, info=info)
|
|
107
|
+
def func(calls=calls, started=started, release=release):
|
|
108
|
+
calls.append(None)
|
|
109
|
+
started.set()
|
|
110
|
+
release.wait(self.TIMEOUT)
|
|
111
|
+
return object()
|
|
112
|
+
|
|
113
|
+
self.run_cond_wait_test(func, cond, started, release, calls)
|
|
114
|
+
|
|
115
|
+
def test_cachedmethod_lock_cond(self):
|
|
116
|
+
for info in (False, True):
|
|
117
|
+
with self.subTest(info=info):
|
|
118
|
+
cache = LRUCache(1)
|
|
119
|
+
lock = Lock()
|
|
120
|
+
cond = WaitCondition(lock)
|
|
121
|
+
started = Event()
|
|
122
|
+
release = Event()
|
|
123
|
+
calls = []
|
|
124
|
+
|
|
125
|
+
class Cached:
|
|
126
|
+
@cachedmethod(
|
|
127
|
+
lambda _: cache,
|
|
128
|
+
lock=lambda _: lock,
|
|
129
|
+
condition=lambda _: cond,
|
|
130
|
+
info=info,
|
|
131
|
+
)
|
|
132
|
+
def method(
|
|
133
|
+
self,
|
|
134
|
+
calls=calls,
|
|
135
|
+
started=started,
|
|
136
|
+
release=release,
|
|
137
|
+
timeout=self.TIMEOUT,
|
|
138
|
+
):
|
|
139
|
+
calls.append(None)
|
|
140
|
+
started.set()
|
|
141
|
+
release.wait(timeout)
|
|
142
|
+
return object()
|
|
143
|
+
|
|
144
|
+
func = Cached().method
|
|
145
|
+
self.run_cond_wait_test(func, cond, started, release, calls)
|
|
146
|
+
|
|
147
|
+
def test_cached_unrelated_lock_cond(self):
|
|
148
|
+
# a condition variable not created with the lock fails on
|
|
149
|
+
# the first cache miss, even without concurrent calls
|
|
150
|
+
for info in (False, True):
|
|
151
|
+
with self.subTest(info=info):
|
|
152
|
+
|
|
153
|
+
@cached(LRUCache(1), lock=Lock(), condition=Condition(), info=info)
|
|
154
|
+
def func():
|
|
155
|
+
return object()
|
|
156
|
+
|
|
157
|
+
with self.assertRaises(RuntimeError):
|
|
158
|
+
func()
|
|
159
|
+
|
|
160
|
+
def test_cachedmethod_unrelated_lock_cond(self):
|
|
161
|
+
for info in (False, True):
|
|
162
|
+
with self.subTest(info=info):
|
|
163
|
+
cache = LRUCache(1)
|
|
164
|
+
lock = Lock()
|
|
165
|
+
cond = Condition()
|
|
166
|
+
|
|
167
|
+
class Cached:
|
|
168
|
+
@cachedmethod(
|
|
169
|
+
lambda _: cache,
|
|
170
|
+
lock=lambda _: lock,
|
|
171
|
+
condition=lambda _: cond,
|
|
172
|
+
info=info,
|
|
173
|
+
)
|
|
174
|
+
def method(self):
|
|
175
|
+
return object()
|
|
176
|
+
|
|
177
|
+
with self.assertRaises(RuntimeError):
|
|
178
|
+
Cached().method()
|
|
179
|
+
|
|
96
180
|
def run_cond_wait_test(self, func, cond, started, release, calls):
|
|
97
181
|
results = [None] * 2
|
|
98
182
|
|
|
@@ -122,14 +206,6 @@ class ThreadingTest(unittest.TestCase):
|
|
|
122
206
|
self.assertEqual(cache_info.hits + cache_info.misses, self.NTHREADS)
|
|
123
207
|
self.assertGreaterEqual(cache_info.misses, 1)
|
|
124
208
|
|
|
125
|
-
def run_cond_test(self, func, cache, info):
|
|
126
|
-
self.run_threading_test(func, cache)
|
|
127
|
-
|
|
128
|
-
if info:
|
|
129
|
-
cache_info = func.cache_info()
|
|
130
|
-
self.assertEqual(cache_info.hits, self.NTHREADS - 1)
|
|
131
|
-
self.assertEqual(cache_info.misses, 1)
|
|
132
|
-
|
|
133
209
|
def run_threading_test(self, func, cache):
|
|
134
210
|
results = [None] * self.NTHREADS
|
|
135
211
|
|
|
@@ -400,3 +400,23 @@ class TLRUCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
400
400
|
cache.timer.tick()
|
|
401
401
|
cache.timer.tick() # past TTL
|
|
402
402
|
self.assertNotIn(42, cache)
|
|
403
|
+
|
|
404
|
+
def test_tlru_default_timer_stubs(self):
|
|
405
|
+
# type stubs must infer key and value types with the default timer
|
|
406
|
+
def ttu(_key: str, value: int, now: float) -> float:
|
|
407
|
+
return now + value
|
|
408
|
+
|
|
409
|
+
cache: TLRUCache[str, int] = TLRUCache(maxsize=2, ttu=ttu)
|
|
410
|
+
|
|
411
|
+
cache["a"] = 3600
|
|
412
|
+
self.assertEqual(3600, cache["a"])
|
|
413
|
+
self.assertEqual(1, len(cache))
|
|
414
|
+
self.assertIn("a", cache)
|
|
415
|
+
|
|
416
|
+
sized: TLRUCache[str, str] = TLRUCache(
|
|
417
|
+
maxsize=10, ttu=lambda _k, _v, now: now + 3600, getsizeof=len
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
sized["abc"] = "abc"
|
|
421
|
+
self.assertEqual("abc", sized["abc"])
|
|
422
|
+
self.assertEqual(3, sized.currsize)
|
|
@@ -272,3 +272,18 @@ class TTLCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
272
272
|
cache.timer.tick()
|
|
273
273
|
cache.timer.tick() # past TTL
|
|
274
274
|
self.assertNotIn(42, cache)
|
|
275
|
+
|
|
276
|
+
def test_ttl_default_timer_stubs(self):
|
|
277
|
+
# type stubs must infer key and value types with the default timer
|
|
278
|
+
cache: TTLCache[str, int] = TTLCache(maxsize=2, ttl=math.inf)
|
|
279
|
+
|
|
280
|
+
cache["a"] = 1
|
|
281
|
+
self.assertEqual(1, cache["a"])
|
|
282
|
+
self.assertEqual(1, len(cache))
|
|
283
|
+
self.assertIn("a", cache)
|
|
284
|
+
|
|
285
|
+
sized: TTLCache[str, str] = TTLCache(maxsize=10, ttl=math.inf, getsizeof=len)
|
|
286
|
+
|
|
287
|
+
sized["abc"] = "abc"
|
|
288
|
+
self.assertEqual("abc", sized["abc"])
|
|
289
|
+
self.assertEqual(3, sized.currsize)
|
|
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
|