1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2017 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing classes used for caching objects.
8"""
9
10from PyQt5.QtCore import QDateTime, QTimer
11
12
13class E5Cache:
14    """
15    Class implementing a LRU cache of a specific size.
16
17    If the maximum number of entries is exceeded, the least recently used item
18    is removed from the cache. A cache hit moves the entry to the front of the
19    cache.
20    """
21    def __init__(self, size=100):
22        """
23        Constructor
24
25        @param size maximum number of entries that may be stored in the cache
26        @type int
27        @exception ValueError raised to indicate an illegal 'size' parameter
28        """
29        if size < 0:
30            raise ValueError("'size' parameter must be positive.")
31
32        self.__size = size
33
34        # internal objects
35        self.__keyList = []
36        self.__store = {}           # stores the cache entries
37        self.__accesStore = {}      # stores the last access date and times
38        self.__hits = 0
39        self.__misses = 0
40        self.__maxsize = 0
41        self.__maxCacheTime = 0     # 0 seconds means aging is disabled
42
43        self.__cacheTimer = QTimer()
44        self.__cacheTimer.setSingleShot(True)
45        self.__cacheTimer.timeout.connect(self.__pruneCache)
46
47    def __moveLast(self, key):
48        """
49        Private method to move a cached item to the MRU position.
50
51        @param key key of the item to be retrieved
52        @type any hashable type that can be used as a dict key
53        """
54        self.__keyList.remove(key)
55        self.__keyList.append(key)
56
57    def __adjustToSize(self):
58        """
59        Private method to adjust the cache to its size.
60        """
61        if self.__size:
62            removeList = self.__keyList[:-self.__size]
63            self.__keyList = self.__keyList[-self.__size:]
64            for key in removeList:
65                del self.__store[key]
66                del self.__accesStore[key]
67        else:
68            self.reset()
69
70    def getSize(self):
71        """
72        Public method to get the maximum size of the cache.
73
74        @return maximum number of entries of the cache
75        @rtype int
76        """
77        return self.__size
78
79    def setSize(self, newSize):
80        """
81        Public method to change the maximum size of the cache.
82
83        @param newSize maximum number of entries that may be stored in the
84            cache
85        @type int
86        """
87        if newSize >= 0:
88            self.__size = newSize
89            self.__adjustToSize()
90
91    def getMaximumCacheTime(self):
92        """
93        Public method to get the maximum time entries may exist in the cache.
94
95        @return maximum cache time in seconds
96        @rtype int
97        """
98        return self.__maxCacheTime
99
100    def setMaximumCacheTime(self, time):
101        """
102        Public method to set the maximum time entries may exist in the cache.
103
104        @param time maximum cache time in seconds
105        @type int
106        """
107        if time != self.__maxCacheTime:
108            self.__cacheTimer.stop()
109            self.__pruneCache()
110            self.__maxCacheTime = time
111            if self.__maxCacheTime > 0:
112                self.__cacheTimer.setInterval(self.__maxCacheTime * 1000)
113                self.__cacheTimer.start()
114
115    def get(self, key):
116        """
117        Public method to get an entry from the cache given its key.
118
119        If the key is present in the cache, it is moved to the MRU position.
120
121        @param key key of the item to be retrieved
122        @type any hashable type that can be used as a dict key
123        @return cached item for the given key or None, if the key is not
124            present
125        @rtype object or None
126        """
127        if key in self.__store:
128            self.__hits += 1
129            self.__moveLast(key)
130            self.__accesStore[key] = QDateTime.currentDateTimeUtc()
131            return self.__store[key]
132        else:
133            self.__misses += 1
134            return None
135
136    def add(self, key, item):
137        """
138        Public method to add an item to the cache.
139
140        If the key is already in use, the cached item is replaced by the new
141        one given and is moved to the MRU position
142
143        @param key key of the item to be retrieved
144        @type any hashable type that can be used as a dict key
145        @param item item to be cached under the given key
146        @type object
147        """
148        if key in self.__store:
149            self.__moveLast(key)
150        else:
151            self.__keyList.append(key)
152        self.__store[key] = item
153        self.__accesStore[key] = QDateTime.currentDateTimeUtc()
154
155        self.__adjustToSize()
156
157        self.__maxsize = max(self.__maxsize, len(self.__keyList))
158
159    def remove(self, key):
160        """
161        Public method to remove an item from the cache.
162
163        @param key key of the item to be retrieved
164        @type any hashable type that can be used as a dict key
165        """
166        if key in self.__store:
167            del self.__store[key]
168            del self.__accesStore[key]
169            self.__keyList.remove(key)
170
171    def clear(self):
172        """
173        Public method to clear the cache.
174        """
175        self.__keyList = []
176        self.__store = {}
177        self.__accesStore = {}
178
179    def reset(self):
180        """
181        Public method to reset the cache.
182
183        This is like clear() but sets the various counters to their initial
184        value as well.
185        """
186        self.clear()
187        self.__hits = 0
188        self.__misses = 0
189        self.__maxsize = 0
190
191    def length(self):
192        """
193        Public method to get the current length of the cache.
194
195        @return current length of the cache
196        @rtype int
197        """
198        return len(self.__keyList)
199
200    def info(self):
201        """
202        Public method to get some information about the cache.
203
204        @return dictionary containing the cache info
205        @rtype dict (with keys "hits", "misses", "maxsize", "currsize")
206        """
207        return {
208            "hits": self.__hits,
209            "misses": self.__misses,
210            "maxsize": self.__maxsize,
211            "currsize": self.length(),
212        }
213
214    def __pruneCache(self):
215        """
216        Private slot to prune outdated cache entries and restart the timer.
217        """
218        if self.__maxCacheTime > 0:
219            current = QDateTime.currentDateTimeUtc()
220
221            keysToBeDeleted = []
222            for key, lastAccessTime in self.__accesStore.items():
223                if lastAccessTime.secsTo(current) > self.__maxCacheTime:
224                    keysToBeDeleted.append(key)
225            for key in keysToBeDeleted:
226                self.remove(key)
227
228            self.__cacheTimer.start()
229