From e7cd4012bc5ea4e4f2c03d5f7beeb58bddd98588 Mon Sep 17 00:00:00 2001 From: fabioz Date: Tue, 4 Mar 2014 10:03:20 -0300 Subject: [PATCH] Changed FiniteCache which had a bug where calling items() would make it recurse forever with a new LRUCache implementation. --- pyqtgraph/graphicsItems/GraphicsItem.py | 26 +----- pyqtgraph/lru_cache.py | 116 ++++++++++++++++++++++++ tests/test.py | 52 ++++++++++- 3 files changed, 170 insertions(+), 24 deletions(-) create mode 100644 pyqtgraph/lru_cache.py diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index e34086bd..5c941dae 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -3,29 +3,9 @@ from ..GraphicsScene import GraphicsScene from ..Point import Point from .. import functions as fn import weakref -from ..pgcollections import OrderedDict -import operator, sys +import operator +from pyqtgraph.lru_cache import LRUCache -class FiniteCache(OrderedDict): - """Caches a finite number of objects, removing - least-frequently used items.""" - def __init__(self, length): - self._length = length - OrderedDict.__init__(self) - - def __setitem__(self, item, val): - self.pop(item, None) # make sure item is added to end - OrderedDict.__setitem__(self, item, val) - while len(self) > self._length: - del self[list(self.keys())[0]] - - def __getitem__(self, item): - val = OrderedDict.__getitem__(self, item) - del self[item] - self[item] = val ## promote this key - return val - - class GraphicsItem(object): """ @@ -38,7 +18,7 @@ class GraphicsItem(object): The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task. """ - _pixelVectorGlobalCache = FiniteCache(100) + _pixelVectorGlobalCache = LRUCache(100, 70) def __init__(self, register=True): if not hasattr(self, '_qtBaseClass'): diff --git a/pyqtgraph/lru_cache.py b/pyqtgraph/lru_cache.py new file mode 100644 index 00000000..862e956a --- /dev/null +++ b/pyqtgraph/lru_cache.py @@ -0,0 +1,116 @@ +import operator +import sys +import itertools + + +_IS_PY3 = sys.version_info[0] == 3 + +class LRUCache(object): + ''' + This LRU cache should be reasonable for short collections (until around 100 items), as it does a + sort on the items if the collection would become too big (so, it is very fast for getting and + setting but when its size would become higher than the max size it does one sort based on the + internal time to decide which items should be removed -- which should be Ok if the resize_to + isn't too close to the max_size so that it becomes an operation that doesn't happen all the + time). + ''' + + def __init__(self, max_size=100, resize_to=70): + ''' + :param int max_size: + This is the maximum size of the cache. When some item is added and the cache would become + bigger than this, it's resized to the value passed on resize_to. + + :param int resize_to: + When a resize operation happens, this is the size of the final cache. + ''' + assert resize_to < max_size + self.max_size = max_size + self.resize_to = resize_to + self._counter = 0 + self._dict = {} + if _IS_PY3: + self._next_time = itertools.count(0).__next__ + else: + self._next_time = itertools.count(0).next + + def __getitem__(self, key): + item = self._dict[key] + item[2] = self._next_time() + return item[1] + + def __len__(self): + return len(self._dict) + + def __setitem__(self, key, value): + item = self._dict.get(key) + if item is None: + if len(self._dict) + 1 > self.max_size: + self._resize_to() + + item = [key, value, self._next_time()] + self._dict[key] = item + else: + item[1] = value + item[2] = self._next_time() + + def __delitem__(self, key): + del self._dict[key] + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + def clear(self): + self._dict.clear() + + if _IS_PY3: + def values(self): + return [i[1] for i in self._dict.values()] + + def keys(self): + return [x[0] for x in self._dict.values()] + + def _resize_to(self): + ordered = sorted(self._dict.values(), key=operator.itemgetter(2))[:self.resize_to] + for i in ordered: + del self._dict[i[0]] + + def iteritems(self, access_time=False): + ''' + :param bool access_time: + If True sorts the returned items by the internal access time. + ''' + if access_time: + for x in sorted(self._dict.values(), key=operator.itemgetter(2)): + yield x[0], x[1] + else: + for x in self._dict.items(): + yield x[0], x[1] + + else: + def values(self): + return [i[1] for i in self._dict.itervalues()] + + def keys(self): + return [x[0] for x in self._dict.itervalues()] + + + def _resize_to(self): + ordered = sorted(self._dict.itervalues(), key=operator.itemgetter(2))[:self.resize_to] + for i in ordered: + del self._dict[i[0]] + + def iteritems(self, access_time=False): + ''' + :param bool access_time: + If True sorts the returned items by the internal access time. + ''' + if access_time: + for x in sorted(self._dict.itervalues(), key=operator.itemgetter(2)): + yield x[0], x[1] + else: + for x in self._dict.iteritems(): + yield x[0], x[1] diff --git a/tests/test.py b/tests/test.py index f24a7d42..9821f821 100644 --- a/tests/test.py +++ b/tests/test.py @@ -5,4 +5,54 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..') ## all tests should be defined with this class so we have the option to tweak it later. class TestCase(unittest.TestCase): - pass \ No newline at end of file + + def testLRU(self): + from pyqtgraph.lru_cache import LRUCache + lru = LRUCache(2, 1) + + def CheckLru(): + lru[1] = 1 + lru[2] = 2 + lru[3] = 3 + + self.assertEqual(2, len(lru)) + self.assertSetEqual(set([2, 3]), set(lru.keys())) + self.assertSetEqual(set([2, 3]), set(lru.values())) + + lru[2] = 2 + self.assertSetEqual(set([2, 3]), set(lru.values())) + + lru[1] = 1 + self.assertSetEqual(set([2, 1]), set(lru.values())) + + #Iterates from the used in the last access to others based on access time. + self.assertEqual([(2, 2), (1, 1)], list(lru.iteritems(access_time=True))) + lru[2] = 2 + self.assertEqual([(1, 1), (2, 2)], list(lru.iteritems(access_time=True))) + + del lru[2] + self.assertEqual([(1, 1), ], list(lru.iteritems(access_time=True))) + + lru[2] = 2 + self.assertEqual([(1, 1), (2, 2)], list(lru.iteritems(access_time=True))) + + _a = lru[1] + self.assertEqual([(2, 2), (1, 1)], list(lru.iteritems(access_time=True))) + + _a = lru[2] + self.assertEqual([(1, 1), (2, 2)], list(lru.iteritems(access_time=True))) + + self.assertEqual(lru.get(2), 2) + self.assertEqual(lru.get(3), None) + self.assertEqual([(1, 1), (2, 2)], list(lru.iteritems(access_time=True))) + + lru.clear() + self.assertEqual([], list(lru.iteritems())) + + CheckLru() + + # Check it twice... + CheckLru() + +if __name__ == '__main__': + unittest.main() \ No newline at end of file