pax_global_header00006660000000000000000000000064142622221630014512gustar00rootroot0000000000000052 comment=8bdcd75a11f629aff4b15ec0ae0faaf165a44fb1 amitdev-lru-dict-8bdcd75/000077500000000000000000000000001426222216300153665ustar00rootroot00000000000000amitdev-lru-dict-8bdcd75/.github/000077500000000000000000000000001426222216300167265ustar00rootroot00000000000000amitdev-lru-dict-8bdcd75/.github/workflows/000077500000000000000000000000001426222216300207635ustar00rootroot00000000000000amitdev-lru-dict-8bdcd75/.github/workflows/build-and-deploy.yml000066400000000000000000000025541426222216300246450ustar00rootroot00000000000000name: Build and deploy on: [push, pull_request] jobs: build_wheels: name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-20.04, windows-2019, macOS-10.15] steps: - uses: actions/checkout@v3 # Used to host cibuildwheel - uses: actions/setup-python@v3 - name: Install cibuildwheel run: python -m pip install cibuildwheel==2.7.0 - name: Build wheels run: python -m cibuildwheel --output-dir wheelhouse - uses: actions/upload-artifact@v3 with: path: ./wheelhouse/*.whl build_sdist: name: Build source distribution runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build sdist run: pipx run build --sdist - uses: actions/upload-artifact@v3 with: path: dist/*.tar.gz pypi_upload: name: Publish to PyPI needs: [build_wheels, build_sdist] runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') steps: - uses: actions/setup-python@v2 - uses: actions/download-artifact@v3 with: name: artifact path: dist - name: Publish package uses: pypa/gh-action-pypi-publish@v1.5.0 with: verbose: true user: __token__ password: ${{ secrets.pypi_password }} amitdev-lru-dict-8bdcd75/.github/workflows/tests.yml000066400000000000000000000013641426222216300226540ustar00rootroot00000000000000name: Tests on: [push] jobs: test: runs-on: ubuntu-latest strategy: matrix: python: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", pypy-3.7, pypy-3.8, pypy-3.9] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - run: pip install .[test] - run: pytest install: runs-on: ubuntu-latest strategy: matrix: python: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", pypy-3.7, pypy-3.8, pypy-3.9] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - run: pip install . - run: python -c "from lru import LRU" amitdev-lru-dict-8bdcd75/.travis.yml000066400000000000000000000002241426222216300174750ustar00rootroot00000000000000language: python python: - "2.7" - "3.6" # command to install dependencies install: - pip install . # command to run tests script: - pytest amitdev-lru-dict-8bdcd75/LICENSE000066400000000000000000000020411426222216300163700ustar00rootroot00000000000000Copyright (c) Amit Dev R Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. amitdev-lru-dict-8bdcd75/MANIFEST.in000066400000000000000000000000601426222216300171200ustar00rootroot00000000000000include LICENSE README.rst MANIFEST MANIFEST.in amitdev-lru-dict-8bdcd75/README.rst000066400000000000000000000067561426222216300170730ustar00rootroot00000000000000.. image:: https://travis-ci.com/amitdev/lru-dict.svg?branch=master :target: https://travis-ci.com/amitdev/lru-dict .. image:: https://github.com/amitdev/lru-dict/actions/workflows/tests.yml/badge.svg :target: https://github.com/amitdev/lru-dict/actions/workflows/tests.yml .. image:: https://github.com/amitdev/lru-dict/actions/workflows/build-and-deploy.yml/badge.svg :target: https://github.com/amitdev/lru-dict/actions/workflows/build-and-deploy.yml LRU Dict ======== A fixed size dict like container which evicts Least Recently Used (LRU) items once size limit is exceeded. There are many python implementations available which does similar things. This is a fast and efficient C implementation. LRU maximum capacity can be modified at run-time. If you are looking for pure python version, look `else where `_. Usage ===== This can be used to build a LRU cache. Usage is almost like a dict. .. code:: python from lru import LRU l = LRU(5) # Create an LRU container that can hold 5 items print l.peek_first_item(), l.peek_last_item() #return the MRU key and LRU key # Would print None None for i in range(5): l[i] = str(i) print l.items() # Prints items in MRU order # Would print [(4, '4'), (3, '3'), (2, '2'), (1, '1'), (0, '0')] print l.peek_first_item(), l.peek_last_item() #return the MRU key and LRU key # Would print (4, '4') (0, '0') l[5] = '5' # Inserting one more item should evict the old item print l.items() # Would print [(5, '5'), (4, '4'), (3, '3'), (2, '2'), (1, '1')] l[3] # Accessing an item would make it MRU print l.items() # Would print [(3, '3'), (5, '5'), (4, '4'), (2, '2'), (1, '1')] # Now 3 is in front l.keys() # Can get keys alone in MRU order # Would print [3, 5, 4, 2, 1] del l[4] # Delete an item print l.items() # Would print [(3, '3'), (5, '5'), (2, '2'), (1, '1')] print l.get_size() # Would print 5 l.set_size(3) print l.items() # Would print [(3, '3'), (5, '5'), (2, '2')] print l.get_size() # Would print 3 print l.has_key(5) # Would print True print 2 in l # Would print True l.get_stats() # Would print (1, 0) l.update(5='0') # Update an item print l.items() # Would print [(5, '0'), (3, '3'), (2, '2')] l.clear() print l.items() # Would print [] def evicted(key, value): print "removing: %s, %s" % (key, value) l = LRU(1, callback=evicted) l[1] = '1' l[2] = '2' # callback would print removing: 1, 1 l[2] = '3' # doesn't call the evicted callback print l.items() # would print [(2, '3')] del l[2] # doesn't call the evicted callback print l.items() # would print [] Install ======= :: pip install lru-dict or :: easy_install lru_dict When to use this ================ Like mentioned above there are many python implementations of an LRU. Use this if you need a faster and memory efficient alternative. It is implemented with a dict and associated linked list to keep track of LRU order. See code for a more detailed explanation. To see an indicative comparison with a pure python module, consider a `benchmark `_ against `pylru `_ (just chosen at random, it should be similar with other python implementations as well). :: $ python bench.py pylru.lrucache Time : 3.31 s, Memory : 453672 Kb $ python bench.py lru.LRU Time : 0.23 s, Memory : 124328 Kb amitdev-lru-dict-8bdcd75/lru.c000066400000000000000000000547741426222216300163550ustar00rootroot00000000000000#include /* * This is a simple implementation of LRU Dict that uses a Python dict and an associated doubly linked * list to keep track of recently inserted/accessed items. * * Dict will store: key -> Node mapping, where Node is a linked list node. * The Node itself will contain the value as well as the key. * * For eg: * * >>> l = LRU(2) * >>> l[0] = 'foo' * >>> l[1] = 'bar' * * can be visualised as: * * ---+--(hash(0)--+--hash(1)--+ * self->dict ...| | | * ---+-----|------+---------|-+ * | | * +-----v------+ +-----v------+ * self->first--->|<'foo'>, <0>|-->|<'bar'>, <1>|<---self->last * +--| |<--| |--+ * | +------------+ +------------+ | * v v * NULL NULL * * The invariant is to maintain the list to reflect the LRU order of items in the dict. * self->first will point to the MRU item and self-last to LRU item. Size of list will not * grow beyond size of LRU dict. * */ #ifndef Py_TYPE #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) #endif #define GET_NODE(d, key) (Node *) Py_TYPE(d)->tp_as_mapping->mp_subscript((d), (key)) #define PUT_NODE(d, key, node) Py_TYPE(d)->tp_as_mapping->mp_ass_subscript((d), (key), ((PyObject *)node)) /* If someone figures out how to enable debug builds with setuptools, you can delete this */ #if 0 #undef assert #define str(s) #s #define assert(v) \ do { \ if (!(v)) { \ fprintf(stderr, "Assertion failed: %s on %s:%d\n", \ str(v), __FILE__, __LINE__); \ fflush(stderr); \ abort(); \ } \ } while(0) #endif typedef struct _Node { PyObject_HEAD PyObject * value; PyObject * key; struct _Node * prev; struct _Node * next; } Node; static void node_dealloc(Node* self) { Py_DECREF(self->key); Py_DECREF(self->value); assert(self->prev == NULL); assert(self->next == NULL); PyObject_Del((PyObject*)self); } static PyObject* node_repr(Node* self) { return PyObject_Repr(self->value); } static PyTypeObject NodeType = { PyVarObject_HEAD_INIT(NULL, 0) "lru.Node", /* tp_name */ sizeof(Node), /* tp_basicsize */ 0, /* tp_itemsize */ (destructor)node_dealloc,/* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_compare */ (reprfunc)node_repr, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT, /* tp_flags */ "Linked List Node", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ 0, /* tp_methods */ 0, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ 0, /* tp_init */ 0, /* tp_alloc */ 0, /* tp_new */ }; typedef struct { PyObject_HEAD PyObject * dict; Node * first; Node * last; Py_ssize_t size; Py_ssize_t hits; Py_ssize_t misses; PyObject *callback; } LRU; static PyObject * set_callback(LRU *self, PyObject *args) { PyObject *result = NULL; PyObject *temp; if (PyArg_ParseTuple(args, "O:set_callback", &temp)) { if (temp == Py_None) { Py_XDECREF(self->callback); self->callback = NULL; } else if (!PyCallable_Check(temp)) { PyErr_SetString(PyExc_TypeError, "parameter must be callable"); return NULL; } else { Py_XINCREF(temp); /* Add a reference to new callback */ Py_XDECREF(self->callback); /* Dispose of previous callback */ self->callback = temp; /* Remember new callback */ } Py_RETURN_NONE; } return result; } static void lru_remove_node(LRU *self, Node* node) { if (self->first == node) { self->first = node->next; } if (self->last == node) { self->last = node->prev; } if (node->prev) { node->prev->next = node->next; } if (node->next) { node->next->prev = node->prev; } node->next = node->prev = NULL; } static void lru_add_node_at_head(LRU *self, Node* node) { node->prev = NULL; if (!self->first) { self->first = self->last = node; node->next = NULL; } else { node->next = self->first; if (node->next) { node->next->prev = node; } self->first = node; } } static void lru_delete_last(LRU *self) { PyObject *arglist; PyObject *result; Node* n = self->last; if (!self->last) return; if (self->callback) { arglist = Py_BuildValue("OO", n->key, n->value); result = PyObject_CallObject(self->callback, arglist); Py_XDECREF(result); Py_DECREF(arglist); } lru_remove_node(self, n); PUT_NODE(self->dict, n->key, NULL); } static Py_ssize_t lru_length(LRU *self) { return PyDict_Size(self->dict); } static PyObject * LRU_contains_key(LRU *self, PyObject *key) { if (PyDict_Contains(self->dict, key)) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; } } static PyObject * LRU_contains(LRU *self, PyObject *args) { PyObject *key; if (!PyArg_ParseTuple(args, "O", &key)) return NULL; return LRU_contains_key(self, key); } static int LRU_seq_contains(LRU *self, PyObject *key) { return PyDict_Contains(self->dict, key); } static PyObject * lru_subscript(LRU *self, register PyObject *key) { Node *node = GET_NODE(self->dict, key); if (!node) { self->misses++; return NULL; } assert(PyObject_TypeCheck(node, &NodeType)); /* We don't need to move the node when it's already self->first. */ if (node != self->first) { lru_remove_node(self, node); lru_add_node_at_head(self, node); } self->hits++; Py_INCREF(node->value); Py_DECREF(node); return node->value; } static PyObject * LRU_get(LRU *self, PyObject *args) { PyObject *key; PyObject *instead = NULL; PyObject *result; if (!PyArg_ParseTuple(args, "O|O", &key, &instead)) return NULL; result = lru_subscript(self, key); PyErr_Clear(); /* GET_NODE sets an exception on miss. Shut it up. */ if (result) return result; if (!instead) { Py_RETURN_NONE; } Py_INCREF(instead); return instead; } static int lru_ass_sub(LRU *self, PyObject *key, PyObject *value) { int res = 0; Node *node = GET_NODE(self->dict, key); PyErr_Clear(); /* GET_NODE sets an exception on miss. Shut it up. */ if (value) { if (node) { Py_INCREF(value); Py_DECREF(node->value); node->value = value; lru_remove_node(self, node); lru_add_node_at_head(self, node); res = 0; } else { node = PyObject_NEW(Node, &NodeType); node->key = key; node->value = value; node->next = node->prev = NULL; Py_INCREF(key); Py_INCREF(value); res = PUT_NODE(self->dict, key, node); if (res == 0) { if (lru_length(self) > self->size) { lru_delete_last(self); } lru_add_node_at_head(self, node); } } } else { res = PUT_NODE(self->dict, key, NULL); if (res == 0) { assert(node && PyObject_TypeCheck(node, &NodeType)); lru_remove_node(self, node); } } Py_XDECREF(node); return res; } static PyMappingMethods LRU_as_mapping = { (lenfunc)lru_length, /*mp_length*/ (binaryfunc)lru_subscript, /*mp_subscript*/ (objobjargproc)lru_ass_sub, /*mp_ass_subscript*/ }; static PyObject * collect(LRU *self, PyObject * (*getterfunc)(Node *)) { register PyObject *v; Node *curr; int i; v = PyList_New(lru_length(self)); if (v == NULL) return NULL; curr = self->first; i = 0; while (curr) { PyList_SET_ITEM(v, i++, getterfunc(curr)); curr = curr->next; } assert(i == lru_length(self)); return v; } static PyObject * get_key(Node *node) { Py_INCREF(node->key); return node->key; } static PyObject * LRU_update(LRU *self, PyObject *args, PyObject *kwargs) { PyObject *key, *value; PyObject *arg = NULL; Py_ssize_t pos = 0; if ((PyArg_ParseTuple(args, "|O", &arg))) { if (arg && PyDict_Check(arg)) { while (PyDict_Next(arg, &pos, &key, &value)) lru_ass_sub(self, key, value); } } if (kwargs != NULL && PyDict_Check(kwargs)) { while (PyDict_Next(kwargs, &pos, &key, &value)) lru_ass_sub(self, key, value); } Py_RETURN_NONE; } static PyObject * LRU_setdefault(LRU *self, PyObject *args) { PyObject *key; PyObject *default_obj = NULL; PyObject *result; if (!PyArg_ParseTuple(args, "O|O", &key, &default_obj)) return NULL; result = lru_subscript(self, key); PyErr_Clear(); if (result) return result; if (!default_obj) default_obj = Py_None; if (lru_ass_sub(self, key, default_obj) != 0) return NULL; Py_INCREF(default_obj); return default_obj; } static PyObject * LRU_pop(LRU *self, PyObject *args) { PyObject *key; PyObject *default_obj = NULL; PyObject *result; if (!PyArg_ParseTuple(args, "O|O", &key, &default_obj)) return NULL; /* Trying to access the item by key. */ result = lru_subscript(self, key); if (result) /* result != NULL, delete it from dict by key */ lru_ass_sub(self, key, NULL); else if (default_obj) { /* result == NULL, i.e. key missing, and default_obj given */ PyErr_Clear(); Py_INCREF(default_obj); result = default_obj; } /* Otherwise (key missing, and default_obj not given [i.e. == NULL]), the * call to lru_subscript (at the location marked by "Trying to access the * item by key" in the comments) has already generated the appropriate * exception. */ return result; } static PyObject * LRU_peek_first_item(LRU *self) { if (self->first) { PyObject *tuple = PyTuple_New(2); Py_INCREF(self->first->key); PyTuple_SET_ITEM(tuple, 0, self->first->key); Py_INCREF(self->first->value); PyTuple_SET_ITEM(tuple, 1, self->first->value); return tuple; } else Py_RETURN_NONE; } static PyObject * LRU_peek_last_item(LRU *self) { if (self->last) { PyObject *tuple = PyTuple_New(2); Py_INCREF(self->last->key); PyTuple_SET_ITEM(tuple, 0, self->last->key); Py_INCREF(self->last->value); PyTuple_SET_ITEM(tuple, 1, self->last->value); return tuple; } else Py_RETURN_NONE; } static PyObject * LRU_popitem(LRU *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = {"least_recent", NULL}; int pop_least_recent = 1; PyObject *result; #if PY_MAJOR_VERSION >= 3 if (!PyArg_ParseTupleAndKeywords(args, kwds, "|p", kwlist, &pop_least_recent)) return NULL; #else { PyObject *arg_ob = Py_True; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &arg_ob)) return NULL; pop_least_recent = PyObject_IsTrue(arg_ob); if (pop_least_recent == -1) return NULL; } #endif if (pop_least_recent) result = LRU_peek_last_item(self); else result = LRU_peek_first_item(self); if (result == Py_None) { PyErr_SetString(PyExc_KeyError, "popitem(): LRU dict is empty"); return NULL; } lru_ass_sub(self, PyTuple_GET_ITEM(result, 0), NULL); Py_INCREF(result); return result; } static PyObject * LRU_keys(LRU *self) { return collect(self, get_key); } static PyObject * get_value(Node *node) { Py_INCREF(node->value); return node->value; } static PyObject * LRU_values(LRU *self) { return collect(self, get_value); } static PyObject * LRU_set_callback(LRU *self, PyObject *args) { return set_callback(self, args); } static PyObject * get_item(Node *node) { PyObject *tuple = PyTuple_New(2); Py_INCREF(node->key); PyTuple_SET_ITEM(tuple, 0, node->key); Py_INCREF(node->value); PyTuple_SET_ITEM(tuple, 1, node->value); return tuple; } static PyObject * LRU_items(LRU *self) { return collect(self, get_item); } static PyObject * LRU_set_size(LRU *self, PyObject *args, PyObject *kwds) { Py_ssize_t newSize; if (!PyArg_ParseTuple(args, "n", &newSize)) { return NULL; } if (newSize <= 0) { PyErr_SetString(PyExc_ValueError, "Size should be a positive number"); return NULL; } while (lru_length(self) > newSize) { lru_delete_last(self); } self->size = newSize; Py_RETURN_NONE; } static PyObject * LRU_clear(LRU *self) { Node *c = self->first; while (c) { Node* n = c; c = c->next; lru_remove_node(self, n); } PyDict_Clear(self->dict); self->hits = 0; self->misses = 0; Py_RETURN_NONE; } static PyObject * LRU_get_size(LRU *self) { return Py_BuildValue("i", self->size); } static PyObject * LRU_get_stats(LRU *self) { return Py_BuildValue("nn", self->hits, self->misses); } /* Hack to implement "key in lru" */ static PySequenceMethods lru_as_sequence = { 0, /* sq_length */ 0, /* sq_concat */ 0, /* sq_repeat */ 0, /* sq_item */ 0, /* sq_slice */ 0, /* sq_ass_item */ 0, /* sq_ass_slice */ (objobjproc) LRU_seq_contains, /* sq_contains */ 0, /* sq_inplace_concat */ 0, /* sq_inplace_repeat */ }; static PyMethodDef LRU_methods[] = { {"__contains__", (PyCFunction)LRU_contains_key, METH_O | METH_COEXIST, PyDoc_STR("L.__contains__(key) -> Check if key is there in L")}, {"keys", (PyCFunction)LRU_keys, METH_NOARGS, PyDoc_STR("L.keys() -> list of L's keys in MRU order")}, {"values", (PyCFunction)LRU_values, METH_NOARGS, PyDoc_STR("L.values() -> list of L's values in MRU order")}, {"items", (PyCFunction)LRU_items, METH_NOARGS, PyDoc_STR("L.items() -> list of L's items (key,value) in MRU order")}, {"has_key", (PyCFunction)LRU_contains, METH_VARARGS, PyDoc_STR("L.has_key(key) -> Check if key is there in L")}, {"get", (PyCFunction)LRU_get, METH_VARARGS, PyDoc_STR("L.get(key, instead) -> If L has key return its value, otherwise instead")}, {"setdefault", (PyCFunction)LRU_setdefault, METH_VARARGS, PyDoc_STR("L.setdefault(key, default=None) -> If L has key return its value, otherwise insert key with a value of default and return default")}, {"pop", (PyCFunction)LRU_pop, METH_VARARGS, PyDoc_STR("L.pop(key[, default]) -> If L has key return its value and remove it from L, otherwise return default. If default is not given and key is not in L, a KeyError is raised.")}, {"popitem", (PyCFunctionWithKeywords)LRU_popitem, METH_VARARGS | METH_KEYWORDS, PyDoc_STR("L.popitem([least_recent=True]) -> Returns and removes a (key, value) pair. The pair returned is the least-recently used if least_recent is true, or the most-recently used if false.")}, {"set_size", (PyCFunction)LRU_set_size, METH_VARARGS, PyDoc_STR("L.set_size() -> set size of LRU")}, {"get_size", (PyCFunction)LRU_get_size, METH_NOARGS, PyDoc_STR("L.get_size() -> get size of LRU")}, {"clear", (PyCFunction)LRU_clear, METH_NOARGS, PyDoc_STR("L.clear() -> clear LRU")}, {"get_stats", (PyCFunction)LRU_get_stats, METH_NOARGS, PyDoc_STR("L.get_stats() -> returns a tuple with cache hits and misses")}, {"peek_first_item", (PyCFunction)LRU_peek_first_item, METH_NOARGS, PyDoc_STR("L.peek_first_item() -> returns the MRU item (key,value) without changing key order")}, {"peek_last_item", (PyCFunction)LRU_peek_last_item, METH_NOARGS, PyDoc_STR("L.peek_last_item() -> returns the LRU item (key,value) without changing key order")}, {"update", (PyCFunction)LRU_update, METH_VARARGS | METH_KEYWORDS, PyDoc_STR("L.update() -> update value for key in LRU")}, {"set_callback", (PyCFunction)LRU_set_callback, METH_VARARGS, PyDoc_STR("L.set_callback(callback) -> set a callback to call when an item is evicted.")}, {NULL, NULL}, }; static PyObject* LRU_repr(LRU* self) { return PyObject_Repr(self->dict); } static int LRU_init(LRU *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = {"size", "callback", NULL}; PyObject *callback = NULL; self->callback = NULL; if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|O", kwlist, &self->size, &callback)) { return -1; } if (callback && callback != Py_None) { if (!PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "parameter must be callable"); return -1; } Py_XINCREF(callback); self->callback = callback; } if ((Py_ssize_t)self->size <= 0) { PyErr_SetString(PyExc_ValueError, "Size should be a positive number"); return -1; } self->dict = PyDict_New(); self->first = self->last = NULL; self->hits = 0; self->misses = 0; return 0; } static void LRU_dealloc(LRU *self) { if (self->dict) { LRU_clear(self); Py_DECREF(self->dict); Py_XDECREF(self->callback); } PyObject_Del((PyObject*)self); } PyDoc_STRVAR(lru_doc, "LRU(size, callback=None) -> new LRU dict that can store up to size elements\n" "An LRU dict behaves like a standard dict, except that it stores only fixed\n" "set of elements. Once the size overflows, it evicts least recently used\n" "items. If a callback is set it will call the callback with the evicted key\n" " and item.\n\n" "Eg:\n" ">>> l = LRU(3)\n" ">>> for i in range(5):\n" ">>> l[i] = str(i)\n" ">>> l.keys()\n" "[2,3,4]\n\n" "Note: An LRU(n) can be thought of as a dict that will have the most\n" "recently accessed n items.\n"); static PyTypeObject LRUType = { PyVarObject_HEAD_INIT(NULL, 0) "lru.LRU", /* tp_name */ sizeof(LRU), /* tp_basicsize */ 0, /* tp_itemsize */ (destructor)LRU_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_compare */ (reprfunc)LRU_repr, /* tp_repr */ 0, /* tp_as_number */ &lru_as_sequence, /* tp_as_sequence */ &LRU_as_mapping, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT, /* tp_flags */ lru_doc, /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ LRU_methods, /* tp_methods */ 0, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ (initproc)LRU_init, /* tp_init */ 0, /* tp_alloc */ 0, /* tp_new */ }; #if PY_MAJOR_VERSION >= 3 static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "lru", /* m_name */ lru_doc, /* m_doc */ -1, /* m_size */ NULL, /* m_methods */ NULL, /* m_reload */ NULL, /* m_traverse */ NULL, /* m_clear */ NULL, /* m_free */ }; #endif static PyObject * moduleinit(void) { PyObject *m; NodeType.tp_new = PyType_GenericNew; if (PyType_Ready(&NodeType) < 0) return NULL; LRUType.tp_new = PyType_GenericNew; if (PyType_Ready(&LRUType) < 0) return NULL; #if PY_MAJOR_VERSION >= 3 m = PyModule_Create(&moduledef); #else m = Py_InitModule3("lru", NULL, lru_doc); #endif if (m == NULL) return NULL; Py_INCREF(&NodeType); Py_INCREF(&LRUType); PyModule_AddObject(m, "LRU", (PyObject *) &LRUType); return m; } #if PY_MAJOR_VERSION < 3 PyMODINIT_FUNC initlru(void) { moduleinit(); } #else PyMODINIT_FUNC PyInit_lru(void) { return moduleinit(); } #endif amitdev-lru-dict-8bdcd75/setup.py000066400000000000000000000024621426222216300171040ustar00rootroot00000000000000from setuptools import setup, Extension module1 = Extension('lru', sources = ['lru.c']) setup (name = 'lru-dict', version = '1.1.8', description = 'An Dict like LRU container.', long_description = open('README.rst').read(), long_description_content_type="text/x-rst", author='Amit Dev', url='https://github.com/amitdev/lru-dict', license='MIT', keywords='lru, dict', ext_modules = [module1], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Operating System :: POSIX', 'Programming Language :: C', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Software Development :: Libraries :: Python Modules', ], extras_require={ 'test': ['pytest'], }, ) amitdev-lru-dict-8bdcd75/test/000077500000000000000000000000001426222216300163455ustar00rootroot00000000000000amitdev-lru-dict-8bdcd75/test/test_lru.py000066400000000000000000000261501426222216300205640ustar00rootroot00000000000000import gc import random import sys import unittest from lru import LRU SIZES = [1, 2, 10, 1000] # Only available on debug python builds. gettotalrefcount = getattr(sys, 'gettotalrefcount', lambda: 0) class TestLRU(unittest.TestCase): def setUp(self): gc.collect() self._before_count = gettotalrefcount() def tearDown(self): after_count = gettotalrefcount() self.assertEqual(self._before_count, after_count) def _check_kvi(self, valid_keys, l): valid_keys = list(valid_keys) valid_vals = list(map(str, valid_keys)) self.assertEqual(valid_keys, l.keys()) self.assertEqual(valid_vals, l.values()) self.assertEqual(list(zip(valid_keys, valid_vals)), l.items()) def test_invalid_size(self): self.assertRaises(ValueError, LRU, -1) self.assertRaises(ValueError, LRU, 0) def test_empty(self): l = LRU(1) self.assertEqual([], l.keys()) self.assertEqual([], l.values()) def test_add_within_size(self): for size in SIZES: l = LRU(size) for i in range(size): l[i] = str(i) self._check_kvi(range(size - 1, -1, -1), l) def test_delete_multiple_within_size(self): for size in SIZES: l = LRU(size) for i in range(size): l[i] = str(i) for i in range(0, size, 2): del l[i] self._check_kvi(range(size - 1, 0, -2), l) for i in range(0, size, 2): with self.assertRaises(KeyError): l[i] def test_delete_multiple(self): for size in SIZES: l = LRU(size) n = size * 2 for i in range(n): l[i] = str(i) for i in range(size, n, 2): del l[i] self._check_kvi(range(n - 1, size, -2), l) for i in range(0, size): with self.assertRaises(KeyError): l[i] for i in range(size, n, 2): with self.assertRaises(KeyError): l[i] def test_add_multiple(self): for size in SIZES: l = LRU(size) for i in range(size): l[i] = str(i) l[size] = str(size) self._check_kvi(range(size, 0, -1), l) def test_access_within_size(self): for size in SIZES: l = LRU(size) for i in range(size): l[i] = str(i) for i in range(size): self.assertEqual(l[i], str(i)) self.assertEqual(l.get(i, None), str(i)) def test_contains(self): for size in SIZES: l = LRU(size) for i in range(size): l[i] = str(i) for i in range(size): self.assertTrue(i in l) def test_access(self): for size in SIZES: l = LRU(size) n = size * 2 for i in range(n): l[i] = str(i) self._check_kvi(range(n - 1, size - 1, -1), l) for i in range(size, n): self.assertEqual(l[i], str(i)) self.assertEqual(l.get(i, None), str(i)) def test_update(self): l = LRU(2) l['a'] = 1 self.assertEqual(l['a'], 1) l.update(a=2) self.assertEqual(l['a'], 2) l['b'] = 2 self.assertEqual(l['b'], 2) l.update(b=3) self.assertEqual(('b', 3), l.peek_first_item()) self.assertEqual(l['a'], 2) self.assertEqual(l['b'], 3) l.update({'a': 1, 'b': 2}) self.assertEqual(('b', 2), l.peek_first_item()) self.assertEqual(l['a'], 1) self.assertEqual(l['b'], 2) l.update() self.assertEqual(('b', 2), l.peek_first_item()) l.update(a=2) self.assertEqual(('a', 2), l.peek_first_item()) def test_peek_first_item(self): l = LRU(2) self.assertEqual(None, l.peek_first_item()) l[1] = '1' l[2] = '2' self.assertEqual((2, '2'), l.peek_first_item()) def test_peek_last_item(self): l = LRU(2) self.assertEqual(None, l.peek_last_item()) l[1] = '1' l[2] = '2' self.assertEqual((1, '1'), l.peek_last_item()) def test_overwrite(self): l = LRU(1) l[1] = '2' l[1] = '1' self.assertEqual('1', l[1]) self._check_kvi([1], l) def test_has_key(self): for size in SIZES: l = LRU(size) for i in range(2 * size): l[i] = str(i) self.assertTrue(l.has_key(i)) for i in range(size, 2 * size): self.assertTrue(l.has_key(i)) for i in range(size): self.assertFalse(l.has_key(i)) def test_capacity_get(self): for size in SIZES: l = LRU(size) self.assertTrue(size == l.get_size()) def test_capacity_set(self): for size in SIZES: l = LRU(size) for i in range(size + 5): l[i] = str(i) l.set_size(size + 10) self.assertTrue(size + 10 == l.get_size()) self.assertTrue(len(l) == size) for i in range(size + 20): l[i] = str(i) self.assertTrue(len(l) == size + 10) l.set_size(size + 10 - 1) self.assertTrue(len(l) == size + 10 - 1) def test_unhashable(self): l = LRU(1) self.assertRaises(TypeError, lambda: l[{'a': 'b'}]) with self.assertRaises(TypeError): l[['1']] = '2' with self.assertRaises(TypeError): del l[{'1': '1'}] def test_clear(self): for size in SIZES: l = LRU(size) for i in range(size + 5): l[i] = str(i) l.clear() for i in range(size): l[i] = str(i) for i in range(size): _ = l[random.randint(0, size - 1)] l.clear() self.assertTrue(len(l) == 0) def test_get_and_del(self): l = LRU(2) l[1] = '1' self.assertEqual('1', l.get(1)) self.assertEqual('1', l.get(2, '1')) self.assertIsNone(l.get(2)) self.assertEqual('1', l[1]) self.assertRaises(KeyError, lambda: l['2']) with self.assertRaises(KeyError): del l['2'] def test_setdefault(self): l = LRU(2) l[1] = '1' val = l.setdefault(1) self.assertEqual('1', val) self.assertEqual((1, 0), l.get_stats()) val = l.setdefault(2, '2') self.assertEqual('2', val) self.assertEqual((1, 1), l.get_stats()) self.assertEqual(val, l[2]) l.clear() val = 'long string' * 512 l.setdefault(1, val) l[2] = '2' l[3] = '3' self.assertTrue(val) def test_pop(self): l = LRU(2) v = '2' * 4096 l[1] = '1' l[2] = v val = l.pop(1) self.assertEqual('1', val) self.assertEqual((1, 0), l.get_stats()) val = l.pop(2, 'not used') self.assertEqual(v, val) del val self.assertTrue(v) self.assertEqual((2, 0), l.get_stats()) val = l.pop(3, '3' * 4096) self.assertEqual('3' * 4096, val) self.assertEqual((2, 1), l.get_stats()) self.assertEqual(0, len(l)) with self.assertRaises(KeyError) as ke: l.pop(4) self.assertEqual(4, ke.args[0]) self.assertEqual((2, 2), l.get_stats()) self.assertEqual(0, len(l)) with self.assertRaises(TypeError): l.pop() def test_popitem(self): l = LRU(3) l[1] = '1' l[2] = '2' l[3] = '3' k, v = l.popitem() self.assertEqual((1, '1'), (k, v)) k, v = l.popitem(least_recent=False) self.assertEqual((3, '3'), (k, v)) self.assertEqual((2, '2'), l.popitem(True)) with self.assertRaises(KeyError) as ke: l.popitem() self.assertEqual('popitem(): LRU dict is empty', ke.args[0]) self.assertEqual((0, 0), l.get_stats()) def test_stats(self): for size in SIZES: l = LRU(size) for i in range(size): l[i] = str(i) self.assertTrue(l.get_stats() == (0, 0)) val = l[0] self.assertTrue(l.get_stats() == (1, 0)) val = l.get(0, None) self.assertTrue(l.get_stats() == (2, 0)) val = l.get(-1, None) self.assertTrue(l.get_stats() == (2, 1)) try: val = l[-1] except: pass self.assertTrue(l.get_stats() == (2, 2)) l.clear() self.assertTrue(len(l) == 0) self.assertTrue(l.get_stats() == (0, 0)) def test_lru(self): l = LRU(1) l['a'] = 1 l['a'] self.assertEqual(l.keys(), ['a']) l['b'] = 2 self.assertEqual(l.keys(), ['b']) l = LRU(2) l['a'] = 1 l['b'] = 2 self.assertEqual(len(l), 2) l['a'] # Testing the first one l['c'] = 3 self.assertEqual(sorted(l.keys()), ['a', 'c']) l['c'] self.assertEqual(sorted(l.keys()), ['a', 'c']) l = LRU(3) l['a'] = 1 l['b'] = 2 l['c'] = 3 self.assertEqual(len(l), 3) l['b'] # Testing the middle one l['d'] = 4 self.assertEqual(sorted(l.keys()), ['b', 'c', 'd']) l['d'] # Testing the last one self.assertEqual(sorted(l.keys()), ['b', 'c', 'd']) l['e'] = 5 self.assertEqual(sorted(l.keys()), ['b', 'd', 'e']) def test_callback(self): counter = [0] first_key = 'a' first_value = 1 def callback(key, value): self.assertEqual(key, first_key) self.assertEqual(value, first_value) counter[0] += 1 l = LRU(1, callback=callback) l[first_key] = first_value l['b'] = 1 # test calling the callback self.assertEqual(counter[0], 1) self.assertEqual(l.keys(), ['b']) l['b'] = 2 # doesn't call callback self.assertEqual(counter[0], 1) self.assertEqual(l.keys(), ['b']) self.assertEqual(l.values(), [2]) l = LRU(1, callback=callback) l[first_key] = first_value l.set_callback(None) l['c'] = 1 # doesn't call callback self.assertEqual(counter[0], 1) self.assertEqual(l.keys(), ['c']) l.set_callback(callback) del l['c'] # doesn't call callback self.assertEqual(counter[0], 1) self.assertEqual(l.keys(), []) l = LRU(2, callback=callback) l['a'] = 1 # test calling the callback l['b'] = 2 # test calling the callback self.assertEqual(counter[0], 1) self.assertEqual(l.keys(), ['b', 'a']) l.set_size(1) self.assertEqual(counter[0], 2) # callback invoked self.assertEqual(l.keys(), ['b']) if __name__ == '__main__': unittest.main()