├── .github └── workflows │ ├── build-and-deploy.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── pyproject.toml ├── setup.py ├── src └── lru │ ├── __init__.py │ ├── __init__.pyi │ ├── _lru.c │ └── py.typed └── test └── test_lru.py /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build_wheels: 7 | name: Build wheels on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-22.04, windows-2022, macos-14] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up QEMU 16 | if: runner.os == 'Linux' 17 | uses: docker/setup-qemu-action@v3 18 | with: 19 | platforms: all 20 | # Used to host cibuildwheel 21 | - name: Build wheels 22 | uses: pypa/cibuildwheel@v2.23.0 23 | env: 24 | # configure cibuildwheel to build native archs ('auto'), and some emulated ones 25 | CIBW_ARCHS_LINUX: auto aarch64 ppc64le 26 | CIBW_ARCHS_MACOS: x86_64 arm64 universal2 27 | CIBW_TEST_REQUIRES: .[test] 28 | CIBW_TEST_COMMAND: pytest {project} 29 | # cannot test the arm64 part for CPython 3.8 universal2/arm64, see https://github.com/pypa/cibuildwheel/pull/1169 30 | CIBW_TEST_SKIP: cp38-macosx_arm64 31 | # pypy will need to be enabled for cibuildwheel 3 32 | CIBW_ENABLE: pypy 33 | - uses: actions/upload-artifact@v4 34 | with: 35 | path: ./wheelhouse/*.whl 36 | name: wheels-${{ matrix.os }} 37 | 38 | build_sdist: 39 | name: Build source distribution 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Build sdist 44 | run: pipx run build --sdist 45 | - uses: actions/upload-artifact@v4 46 | with: 47 | path: dist/*.tar.gz 48 | name: source-dist 49 | 50 | pypi_upload: 51 | name: Publish to PyPI 52 | needs: [build_wheels, build_sdist] 53 | runs-on: ubuntu-latest 54 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 55 | steps: 56 | - uses: actions/setup-python@v5 57 | - uses: actions/download-artifact@v4 58 | with: 59 | path: dist 60 | - name: Publish package 61 | uses: pypa/gh-action-pypi-publish@release/v1 62 | with: 63 | verbose: true 64 | user: __token__ 65 | password: ${{ secrets.pypi_password }} 66 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-22.04 8 | strategy: 9 | matrix: 10 | python: 11 | [ 12 | "3.8", 13 | "3.9", 14 | "3.10", 15 | "3.11", 16 | "3.12", 17 | "3.13", 18 | "pypy-3.8", 19 | "pypy-3.9", 20 | "pypy-3.10", 21 | "pypy-3.11", 22 | ] 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python }} 29 | - run: pip install .[test] 30 | - run: pytest 31 | 32 | install: 33 | runs-on: ubuntu-22.04 34 | strategy: 35 | matrix: 36 | python: 37 | [ 38 | "3.8", 39 | "3.9", 40 | "3.10", 41 | "3.11", 42 | "3.12", 43 | "3.13", 44 | "pypy-3.8", 45 | "pypy-3.9", 46 | "pypy-3.10", 47 | "pypy-3.11", 48 | ] 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: actions/setup-python@v5 53 | with: 54 | python-version: ${{ matrix.python }} 55 | - run: pip install . 56 | - run: python -c "from lru import LRU" 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # pdm config 132 | .pdm-python -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Amit Dev R 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.rst MANIFEST MANIFEST.in 2 | graft src 3 | global-exclude *.pyc 4 | global-exclude *.cache -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/amitdev/lru-dict/actions/workflows/tests.yml/badge.svg 2 | :target: https://github.com/amitdev/lru-dict/actions/workflows/tests.yml 3 | 4 | .. image:: https://github.com/amitdev/lru-dict/actions/workflows/build-and-deploy.yml/badge.svg 5 | :target: https://github.com/amitdev/lru-dict/actions/workflows/build-and-deploy.yml 6 | 7 | LRU Dict 8 | ======== 9 | 10 | A fixed size dict like container which evicts Least Recently Used (LRU) items 11 | once size limit is exceeded. There are many python implementations available 12 | which does similar things. This is a fast and efficient C implementation. 13 | LRU maximum capacity can be modified at run-time. 14 | If you are looking for pure python version, look `else where `_. 15 | 16 | Usage 17 | ===== 18 | 19 | This can be used to build a LRU cache. Usage is almost like a dict. 20 | 21 | .. code:: python3 22 | 23 | from lru import LRU 24 | l = LRU(5) # Create an LRU container that can hold 5 items 25 | 26 | print l.peek_first_item(), l.peek_last_item() #return the MRU key and LRU key 27 | # Would print None None 28 | 29 | for i in range(5): 30 | l[i] = str(i) 31 | print l.items() # Prints items in MRU order 32 | # Would print [(4, '4'), (3, '3'), (2, '2'), (1, '1'), (0, '0')] 33 | 34 | print l.peek_first_item(), l.peek_last_item() #return the MRU key and LRU key 35 | # Would print (4, '4') (0, '0') 36 | 37 | l[5] = '5' # Inserting one more item should evict the old item 38 | print l.items() 39 | # Would print [(5, '5'), (4, '4'), (3, '3'), (2, '2'), (1, '1')] 40 | 41 | l[3] # Accessing an item would make it MRU 42 | print l.items() 43 | # Would print [(3, '3'), (5, '5'), (4, '4'), (2, '2'), (1, '1')] 44 | # Now 3 is in front 45 | 46 | l.keys() # Can get keys alone in MRU order 47 | # Would print [3, 5, 4, 2, 1] 48 | 49 | del l[4] # Delete an item 50 | print l.items() 51 | # Would print [(3, '3'), (5, '5'), (2, '2'), (1, '1')] 52 | 53 | print l.get_size() 54 | # Would print 5 55 | 56 | l.set_size(3) 57 | print l.items() 58 | # Would print [(3, '3'), (5, '5'), (2, '2')] 59 | print l.get_size() 60 | # Would print 3 61 | print l.has_key(5) 62 | # Would print True 63 | print 2 in l 64 | # Would print True 65 | 66 | l.get_stats() 67 | # Would print (1, 0) 68 | 69 | 70 | l.update(5='0') # Update an item 71 | print l.items() 72 | # Would print [(5, '0'), (3, '3'), (2, '2')] 73 | 74 | l.clear() 75 | print l.items() 76 | # Would print [] 77 | 78 | def evicted(key, value): 79 | print "removing: %s, %s" % (key, value) 80 | 81 | l = LRU(1, callback=evicted) 82 | 83 | l[1] = '1' 84 | l[2] = '2' 85 | # callback would print removing: 1, 1 86 | 87 | l[2] = '3' 88 | # doesn't call the evicted callback 89 | 90 | print l.items() 91 | # would print [(2, '3')] 92 | 93 | del l[2] 94 | # doesn't call the evicted callback 95 | 96 | print l.items() 97 | # would print [] 98 | 99 | Install 100 | ======= 101 | 102 | :: 103 | 104 | pip install lru-dict 105 | 106 | or 107 | 108 | :: 109 | 110 | easy_install lru_dict 111 | 112 | 113 | When to use this 114 | ================ 115 | 116 | Like mentioned above there are many python implementations of an LRU. Use this 117 | if you need a faster and memory efficient alternative. It is implemented with a 118 | dict and associated linked list to keep track of LRU order. See code for a more 119 | detailed explanation. To see an indicative comparison with a pure python module, 120 | consider a `benchmark `_ against 121 | `pylru `_ (just chosen at random, it should 122 | be similar with other python implementations as well). 123 | 124 | :: 125 | 126 | $ python bench.py pylru.lrucache 127 | Time : 3.31 s, Memory : 453672 Kb 128 | $ python bench.py lru.LRU 129 | Time : 0.23 s, Memory : 124328 Kb 130 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "lru-dict" 3 | version = "1.3.0" 4 | description = "An Dict like LRU container." 5 | authors = [ 6 | {name = "Amit Dev"}, 7 | ] 8 | dependencies = [] 9 | requires-python = ">=3.8" 10 | readme = "README.rst" 11 | license = {text = "MIT"} 12 | keywords = ["lru", "dict"] 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | "Operating System :: POSIX", 19 | "Programming Language :: C", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: Implementation :: CPython", 27 | "Topic :: Software Development :: Libraries :: Python Modules", 28 | ] 29 | [project.urls] 30 | Homepage = "https://github.com/amitdev/lru-dict" 31 | [project.optional-dependencies] 32 | test = [ 33 | "pytest", 34 | ] 35 | 36 | [build-system] 37 | requires = ["setuptools>=61", "wheel"] 38 | build-backend = "setuptools.build_meta" 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension 2 | 3 | extensions = [ 4 | Extension("lru._lru", ["src/lru/_lru.c"]), 5 | ] 6 | 7 | args = { 8 | "include_package_data": True, 9 | "exclude_package_data": {"": ["*.c"]}, 10 | } 11 | 12 | setup(ext_modules=extensions, **args) 13 | -------------------------------------------------------------------------------- /src/lru/__init__.py: -------------------------------------------------------------------------------- 1 | from ._lru import LRU as LRU # noqa: F401 2 | 3 | __all__ = ["LRU"] 4 | -------------------------------------------------------------------------------- /src/lru/__init__.pyi: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Any, 3 | Callable, 4 | Generic, 5 | Hashable, 6 | Iterable, 7 | TypeVar, 8 | overload, 9 | Protocol 10 | ) 11 | 12 | _KT = TypeVar("_KT", bound=Hashable) 13 | _VT = TypeVar("_VT") 14 | _VT_co = TypeVar("_VT_co", covariant=True) 15 | _T = TypeVar("_T") 16 | 17 | 18 | class __SupportsKeysAndGetItem(Protocol[_KT, _VT_co]): 19 | def keys(self) -> Iterable[_KT]: ... 20 | def __getitem__(self, __key: _KT) -> _VT_co: ... 21 | 22 | 23 | class LRU(Generic[_KT, _VT]): 24 | @overload 25 | def __init__(self, size: int) -> None: ... 26 | @overload 27 | def __init__(self, size: int, callback: Callable[[_KT, _VT], Any]) -> None: ... 28 | def clear(self) -> None: ... 29 | @overload 30 | def get(self, key: _KT) -> _VT | None: ... 31 | @overload 32 | def get(self, key: _KT, instead: _VT | _T) -> _VT | _T: ... 33 | def get_size(self) -> int: ... 34 | def has_key(self, key: _KT) -> bool: ... 35 | def keys(self) -> list[_KT]: ... 36 | def values(self) -> list[_VT]: ... 37 | def items(self) -> list[tuple[_KT, _VT]]: ... 38 | def peek_first_item(self) -> tuple[_KT, _VT] | None: ... 39 | def peek_last_item(self) -> tuple[_KT, _VT] | None: ... 40 | @overload 41 | def pop(self, key: _KT) -> _VT | None: ... 42 | @overload 43 | def pop(self, key: _KT, default: _VT | _T) -> _VT | _T: ... 44 | def popitem(self, least_recent: bool = ...) -> tuple[_KT, _VT]: ... 45 | @overload 46 | def setdefault(self: LRU[_KT, _T | None], key: _KT) -> _T | None: ... 47 | @overload 48 | def setdefault(self, key: _KT, default: _VT) -> _VT: ... 49 | def set_callback(self, callback: Callable[[_KT, _VT], Any] | None) -> None: ... 50 | def set_size(self, size: int) -> None: ... 51 | @overload 52 | def update(self, __m: __SupportsKeysAndGetItem[_KT, _VT], **kwargs: _VT) -> None: ... 53 | @overload 54 | def update(self, __m: Iterable[tuple[_KT, _VT]], **kwargs: _VT) -> None: ... 55 | @overload 56 | def update(self, **kwargs: _VT) -> None: ... 57 | def get_stats(self) -> tuple[int, int]: ... 58 | def __contains__(self, __o: Any) -> bool: ... 59 | def __delitem__(self, key: _KT) -> None: ... 60 | def __getitem__(self, item: _KT) -> _VT: ... 61 | def __len__(self) -> int: ... 62 | def __repr__(self) -> str: ... 63 | def __setitem__(self, key: _KT, value: _VT) -> None: ... 64 | -------------------------------------------------------------------------------- /src/lru/_lru.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /* 4 | * This is a simple implementation of LRU Dict that uses a Python dict and an associated doubly linked 5 | * list to keep track of recently inserted/accessed items. 6 | * 7 | * Dict will store: key -> Node mapping, where Node is a linked list node. 8 | * The Node itself will contain the value as well as the key. 9 | * 10 | * For eg: 11 | * 12 | * >>> l = LRU(2) 13 | * >>> l[0] = 'foo' 14 | * >>> l[1] = 'bar' 15 | * 16 | * can be visualised as: 17 | * 18 | * ---+--(hash(0)--+--hash(1)--+ 19 | * self->dict ...| | | 20 | * ---+-----|------+---------|-+ 21 | * | | 22 | * +-----v------+ +-----v------+ 23 | * self->first--->|<'foo'>, <0>|-->|<'bar'>, <1>|<---self->last 24 | * +--| |<--| |--+ 25 | * | +------------+ +------------+ | 26 | * v v 27 | * NULL NULL 28 | * 29 | * The invariant is to maintain the list to reflect the LRU order of items in the dict. 30 | * self->first will point to the MRU item and self-last to LRU item. Size of list will not 31 | * grow beyond size of LRU dict. 32 | * 33 | */ 34 | 35 | #ifndef Py_TYPE 36 | #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) 37 | #endif 38 | 39 | #define GET_NODE(d, key) (Node *) Py_TYPE(d)->tp_as_mapping->mp_subscript((d), (key)) 40 | #define PUT_NODE(d, key, node) Py_TYPE(d)->tp_as_mapping->mp_ass_subscript((d), (key), ((PyObject *)node)) 41 | 42 | /* If someone figures out how to enable debug builds with setuptools, you can delete this */ 43 | #if 0 44 | #undef assert 45 | #define str(s) #s 46 | #define assert(v) \ 47 | do { \ 48 | if (!(v)) { \ 49 | fprintf(stderr, "Assertion failed: %s on %s:%d\n", \ 50 | str(v), __FILE__, __LINE__); \ 51 | fflush(stderr); \ 52 | abort(); \ 53 | } \ 54 | } while(0) 55 | #endif 56 | 57 | typedef struct _Node { 58 | PyObject_HEAD 59 | PyObject * value; 60 | PyObject * key; 61 | struct _Node * prev; 62 | struct _Node * next; 63 | } Node; 64 | 65 | static void 66 | node_dealloc(Node* self) 67 | { 68 | Py_DECREF(self->key); 69 | Py_DECREF(self->value); 70 | assert(self->prev == NULL); 71 | assert(self->next == NULL); 72 | Py_TYPE(self)->tp_free((PyObject *)self); 73 | } 74 | 75 | static PyObject* 76 | node_repr(Node* self) 77 | { 78 | return PyObject_Repr(self->value); 79 | } 80 | 81 | static PyTypeObject NodeType = { 82 | PyVarObject_HEAD_INIT(NULL, 0) 83 | "_lru.Node", /* tp_name */ 84 | sizeof(Node), /* tp_basicsize */ 85 | 0, /* tp_itemsize */ 86 | (destructor)node_dealloc,/* tp_dealloc */ 87 | 0, /* tp_print */ 88 | 0, /* tp_getattr */ 89 | 0, /* tp_setattr */ 90 | 0, /* tp_compare */ 91 | (reprfunc)node_repr, /* tp_repr */ 92 | 0, /* tp_as_number */ 93 | 0, /* tp_as_sequence */ 94 | 0, /* tp_as_mapping */ 95 | 0, /* tp_hash */ 96 | 0, /* tp_call */ 97 | 0, /* tp_str */ 98 | 0, /* tp_getattro */ 99 | 0, /* tp_setattro */ 100 | 0, /* tp_as_buffer */ 101 | Py_TPFLAGS_DEFAULT, /* tp_flags */ 102 | "Linked List Node", /* tp_doc */ 103 | 0, /* tp_traverse */ 104 | 0, /* tp_clear */ 105 | 0, /* tp_richcompare */ 106 | 0, /* tp_weaklistoffset */ 107 | 0, /* tp_iter */ 108 | 0, /* tp_iternext */ 109 | 0, /* tp_methods */ 110 | 0, /* tp_members */ 111 | 0, /* tp_getset */ 112 | 0, /* tp_base */ 113 | 0, /* tp_dict */ 114 | 0, /* tp_descr_get */ 115 | 0, /* tp_descr_set */ 116 | 0, /* tp_dictoffset */ 117 | 0, /* tp_init */ 118 | 0, /* tp_alloc */ 119 | 0, /* tp_new */ 120 | }; 121 | 122 | typedef struct { 123 | PyObject_HEAD 124 | PyObject * dict; 125 | Node * first; 126 | Node * last; 127 | Py_ssize_t size; 128 | Py_ssize_t hits; 129 | Py_ssize_t misses; 130 | PyObject *callback; 131 | } LRU; 132 | 133 | 134 | static PyObject * 135 | set_callback(LRU *self, PyObject *args) 136 | { 137 | PyObject *result = NULL; 138 | PyObject *temp; 139 | 140 | if (PyArg_ParseTuple(args, "O:set_callback", &temp)) { 141 | if (temp == Py_None) { 142 | Py_XDECREF(self->callback); 143 | self->callback = NULL; 144 | } else if (!PyCallable_Check(temp)) { 145 | PyErr_SetString(PyExc_TypeError, "parameter must be callable"); 146 | return NULL; 147 | } else { 148 | Py_XINCREF(temp); /* Add a reference to new callback */ 149 | Py_XDECREF(self->callback); /* Dispose of previous callback */ 150 | self->callback = temp; /* Remember new callback */ 151 | } 152 | Py_RETURN_NONE; 153 | } 154 | return result; 155 | } 156 | 157 | static void 158 | lru_remove_node(LRU *self, Node* node) 159 | { 160 | if (self->first == node) { 161 | self->first = node->next; 162 | } 163 | if (self->last == node) { 164 | self->last = node->prev; 165 | } 166 | if (node->prev) { 167 | node->prev->next = node->next; 168 | } 169 | if (node->next) { 170 | node->next->prev = node->prev; 171 | } 172 | node->next = node->prev = NULL; 173 | } 174 | 175 | static void 176 | lru_add_node_at_head(LRU *self, Node* node) 177 | { 178 | node->prev = NULL; 179 | if (!self->first) { 180 | self->first = self->last = node; 181 | node->next = NULL; 182 | } else { 183 | node->next = self->first; 184 | if (node->next) { 185 | node->next->prev = node; 186 | } 187 | self->first = node; 188 | } 189 | } 190 | 191 | static void 192 | lru_delete_last(LRU *self) 193 | { 194 | PyObject *arglist; 195 | PyObject *result; 196 | Node* n = self->last; 197 | 198 | if (!self->last) 199 | return; 200 | 201 | if (self->callback) { 202 | arglist = Py_BuildValue("OO", n->key, n->value); 203 | lru_remove_node(self, n); 204 | result = PyObject_CallObject(self->callback, arglist); 205 | Py_XDECREF(result); 206 | Py_DECREF(arglist); 207 | } 208 | else { 209 | lru_remove_node(self, n); 210 | } 211 | PUT_NODE(self->dict, n->key, NULL); 212 | } 213 | 214 | static inline Py_ssize_t 215 | lru_length(LRU *self) 216 | { 217 | return PyDict_Size(self->dict); 218 | } 219 | 220 | static PyObject * 221 | LRU_contains_key(LRU *self, PyObject *key) 222 | { 223 | if (PyDict_Contains(self->dict, key)) 224 | Py_RETURN_TRUE; 225 | Py_RETURN_FALSE; 226 | } 227 | 228 | static PyObject * 229 | LRU_contains(LRU *self, PyObject *args) 230 | { 231 | PyObject *key; 232 | if (!PyArg_ParseTuple(args, "O", &key)) 233 | return NULL; 234 | return LRU_contains_key(self, key); 235 | } 236 | 237 | static int 238 | LRU_seq_contains(LRU *self, PyObject *key) 239 | { 240 | return PyDict_Contains(self->dict, key); 241 | } 242 | 243 | static PyObject * 244 | lru_subscript(LRU *self, register PyObject *key) 245 | { 246 | Node *node = GET_NODE(self->dict, key); 247 | if (!node) { 248 | self->misses++; 249 | return NULL; 250 | } 251 | 252 | assert(PyObject_TypeCheck(node, &NodeType)); 253 | 254 | /* We don't need to move the node when it's already self->first. */ 255 | if (node != self->first) { 256 | lru_remove_node(self, node); 257 | lru_add_node_at_head(self, node); 258 | } 259 | 260 | self->hits++; 261 | Py_INCREF(node->value); 262 | Py_DECREF(node); 263 | return node->value; 264 | } 265 | 266 | static PyObject * 267 | LRU_get(LRU *self, PyObject *args, PyObject *keywds) 268 | { 269 | PyObject *key; 270 | PyObject *default_obj = NULL; 271 | PyObject *result; 272 | 273 | static char *kwlist[] = {"key", "default", NULL}; 274 | if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|O", kwlist, &key, &default_obj)) 275 | return NULL; 276 | 277 | result = lru_subscript(self, key); 278 | PyErr_Clear(); /* GET_NODE sets an exception on miss. Shut it up. */ 279 | if (result) 280 | return result; 281 | 282 | if (!default_obj) 283 | Py_RETURN_NONE; 284 | 285 | Py_INCREF(default_obj); 286 | return default_obj; 287 | } 288 | 289 | static int 290 | lru_ass_sub(LRU *self, PyObject *key, PyObject *value) 291 | { 292 | int res = 0; 293 | Node *node = GET_NODE(self->dict, key); 294 | PyErr_Clear(); /* GET_NODE sets an exception on miss. Shut it up. */ 295 | 296 | if (value) { 297 | if (node) { 298 | Py_INCREF(value); 299 | Py_DECREF(node->value); 300 | node->value = value; 301 | 302 | lru_remove_node(self, node); 303 | lru_add_node_at_head(self, node); 304 | 305 | res = 0; 306 | } else { 307 | node = PyObject_NEW(Node, &NodeType); 308 | node->key = key; 309 | node->value = value; 310 | node->next = node->prev = NULL; 311 | 312 | Py_INCREF(key); 313 | Py_INCREF(value); 314 | 315 | res = PUT_NODE(self->dict, key, node); 316 | if (res == 0) { 317 | if (lru_length(self) > self->size) { 318 | lru_delete_last(self); 319 | } 320 | 321 | lru_add_node_at_head(self, node); 322 | } 323 | } 324 | } else { 325 | res = PUT_NODE(self->dict, key, NULL); 326 | if (res == 0) { 327 | assert(node && PyObject_TypeCheck(node, &NodeType)); 328 | lru_remove_node(self, node); 329 | } 330 | } 331 | 332 | Py_XDECREF(node); 333 | return res; 334 | } 335 | 336 | static PyMappingMethods LRU_as_mapping = { 337 | (lenfunc)lru_length, /*mp_length*/ 338 | (binaryfunc)lru_subscript, /*mp_subscript*/ 339 | (objobjargproc)lru_ass_sub, /*mp_ass_subscript*/ 340 | }; 341 | 342 | static PyObject * 343 | collect(LRU *self, PyObject * (*getterfunc)(Node *)) 344 | { 345 | register PyObject *v; 346 | Node *curr; 347 | int i; 348 | v = PyList_New(lru_length(self)); 349 | if (v == NULL) 350 | return NULL; 351 | curr = self->first; 352 | i = 0; 353 | 354 | while (curr) { 355 | PyList_SET_ITEM(v, i++, getterfunc(curr)); 356 | curr = curr->next; 357 | } 358 | assert(i == lru_length(self)); 359 | return v; 360 | } 361 | 362 | static PyObject * 363 | get_key(Node *node) 364 | { 365 | Py_INCREF(node->key); 366 | return node->key; 367 | } 368 | 369 | static PyObject * 370 | LRU_update(LRU *self, PyObject *args, PyObject *kwargs) 371 | { 372 | PyObject *key, *value; 373 | PyObject *arg = NULL; 374 | Py_ssize_t pos = 0; 375 | 376 | if ((PyArg_ParseTuple(args, "|O", &arg))) { 377 | if (arg && PyDict_Check(arg)) { 378 | while (PyDict_Next(arg, &pos, &key, &value)) 379 | lru_ass_sub(self, key, value); 380 | } 381 | } 382 | 383 | if (kwargs != NULL && PyDict_Check(kwargs)) { 384 | while (PyDict_Next(kwargs, &pos, &key, &value)) 385 | lru_ass_sub(self, key, value); 386 | } 387 | 388 | Py_RETURN_NONE; 389 | } 390 | 391 | static PyObject * 392 | LRU_setdefault(LRU *self, PyObject *args) 393 | { 394 | PyObject *key; 395 | PyObject *default_obj = NULL; 396 | PyObject *result; 397 | 398 | if (!PyArg_ParseTuple(args, "O|O", &key, &default_obj)) 399 | return NULL; 400 | 401 | result = lru_subscript(self, key); 402 | PyErr_Clear(); 403 | if (result) 404 | return result; 405 | 406 | if (!default_obj) 407 | default_obj = Py_None; 408 | 409 | if (lru_ass_sub(self, key, default_obj) != 0) 410 | return NULL; 411 | 412 | Py_INCREF(default_obj); 413 | return default_obj; 414 | } 415 | 416 | static PyObject * 417 | LRU_pop(LRU *self, PyObject *args, PyObject *keywds) 418 | { 419 | PyObject *key; 420 | PyObject *default_obj = NULL; 421 | PyObject *result; 422 | 423 | static char *kwlist[] = {"key", "default", NULL}; 424 | if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|O", kwlist, &key, &default_obj)) 425 | return NULL; 426 | 427 | /* Trying to access the item by key. */ 428 | result = lru_subscript(self, key); 429 | 430 | if (result) 431 | /* result != NULL, delete it from dict by key */ 432 | lru_ass_sub(self, key, NULL); 433 | else if (default_obj) { 434 | /* result == NULL, i.e. key missing, and default_obj given */ 435 | PyErr_Clear(); 436 | Py_INCREF(default_obj); 437 | result = default_obj; 438 | } 439 | /* Otherwise (key missing, and default_obj not given [i.e. == NULL]), the 440 | * call to lru_subscript (at the location marked by "Trying to access the 441 | * item by key" in the comments) has already generated the appropriate 442 | * exception. */ 443 | 444 | return result; 445 | } 446 | 447 | static PyObject * 448 | LRU_peek_first_item(LRU *self) 449 | { 450 | if (self->first) 451 | return Py_BuildValue("OO", self->first->key, self->first->value); 452 | Py_RETURN_NONE; 453 | } 454 | 455 | static PyObject * 456 | LRU_peek_last_item(LRU *self) 457 | { 458 | if (self->last) 459 | return Py_BuildValue("OO", self->last->key, self->last->value); 460 | Py_RETURN_NONE; 461 | } 462 | 463 | static PyObject * 464 | LRU_popitem(LRU *self, PyObject *args, PyObject *kwds) 465 | { 466 | static char *kwlist[] = {"least_recent", NULL}; 467 | int pop_least_recent = 1; 468 | PyObject *result; 469 | 470 | #if PY_MAJOR_VERSION >= 3 471 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "|p", kwlist, &pop_least_recent)) 472 | return NULL; 473 | #else 474 | { 475 | PyObject *arg_ob = Py_True; 476 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &arg_ob)) 477 | return NULL; 478 | pop_least_recent = PyObject_IsTrue(arg_ob); 479 | if (pop_least_recent == -1) 480 | return NULL; 481 | } 482 | #endif 483 | if (pop_least_recent) 484 | result = LRU_peek_last_item(self); 485 | else 486 | result = LRU_peek_first_item(self); 487 | if (result == Py_None) { 488 | PyErr_SetString(PyExc_KeyError, "popitem(): LRU dict is empty"); 489 | return NULL; 490 | } 491 | lru_ass_sub(self, PyTuple_GET_ITEM(result, 0), NULL); 492 | Py_INCREF(result); 493 | return result; 494 | } 495 | 496 | static PyObject * 497 | LRU_keys(LRU *self) { 498 | return collect(self, get_key); 499 | } 500 | 501 | static PyObject * 502 | get_value(Node *node) 503 | { 504 | Py_INCREF(node->value); 505 | return node->value; 506 | } 507 | 508 | static PyObject * 509 | LRU_values(LRU *self) 510 | { 511 | return collect(self, get_value); 512 | } 513 | 514 | static PyObject * 515 | LRU_set_callback(LRU *self, PyObject *args) 516 | { 517 | return set_callback(self, args); 518 | } 519 | 520 | static PyObject * 521 | get_item(Node *node) 522 | { 523 | PyObject *tuple = PyTuple_New(2); 524 | Py_INCREF(node->key); 525 | PyTuple_SET_ITEM(tuple, 0, node->key); 526 | Py_INCREF(node->value); 527 | PyTuple_SET_ITEM(tuple, 1, node->value); 528 | return tuple; 529 | } 530 | 531 | static PyObject * 532 | LRU_items(LRU *self) 533 | { 534 | return collect(self, get_item); 535 | } 536 | 537 | static PyObject * 538 | LRU_set_size(LRU *self, PyObject *args, PyObject *kwds) 539 | { 540 | Py_ssize_t newSize; 541 | if (!PyArg_ParseTuple(args, "n", &newSize)) { 542 | return NULL; 543 | } 544 | if (newSize <= 0) { 545 | PyErr_SetString(PyExc_ValueError, "Size should be a positive number"); 546 | return NULL; 547 | } 548 | while (lru_length(self) > newSize) { 549 | lru_delete_last(self); 550 | } 551 | self->size = newSize; 552 | Py_RETURN_NONE; 553 | } 554 | 555 | static PyObject * 556 | LRU_clear(LRU *self) 557 | { 558 | Node *c = self->first; 559 | 560 | while (c) { 561 | Node* n = c; 562 | c = c->next; 563 | lru_remove_node(self, n); 564 | } 565 | PyDict_Clear(self->dict); 566 | 567 | self->hits = 0; 568 | self->misses = 0; 569 | Py_RETURN_NONE; 570 | } 571 | 572 | 573 | static PyObject * 574 | LRU_get_size(LRU *self) 575 | { 576 | return Py_BuildValue("i", self->size); 577 | } 578 | 579 | static PyObject * 580 | LRU_get_stats(LRU *self) 581 | { 582 | return Py_BuildValue("nn", self->hits, self->misses); 583 | } 584 | 585 | 586 | /* Hack to implement "key in lru" */ 587 | static PySequenceMethods lru_as_sequence = { 588 | 0, /* sq_length */ 589 | 0, /* sq_concat */ 590 | 0, /* sq_repeat */ 591 | 0, /* sq_item */ 592 | 0, /* sq_slice */ 593 | 0, /* sq_ass_item */ 594 | 0, /* sq_ass_slice */ 595 | (objobjproc) LRU_seq_contains, /* sq_contains */ 596 | 0, /* sq_inplace_concat */ 597 | 0, /* sq_inplace_repeat */ 598 | }; 599 | 600 | static PyMethodDef LRU_methods[] = { 601 | {"__contains__", (PyCFunction)LRU_contains_key, METH_O | METH_COEXIST, 602 | PyDoc_STR("L.__contains__(key) -> Check if key is there in L")}, 603 | {"keys", (PyCFunction)LRU_keys, METH_NOARGS, 604 | PyDoc_STR("L.keys() -> list of L's keys in MRU order")}, 605 | {"values", (PyCFunction)LRU_values, METH_NOARGS, 606 | PyDoc_STR("L.values() -> list of L's values in MRU order")}, 607 | {"items", (PyCFunction)LRU_items, METH_NOARGS, 608 | PyDoc_STR("L.items() -> list of L's items (key,value) in MRU order")}, 609 | {"has_key", (PyCFunction)LRU_contains, METH_VARARGS, 610 | PyDoc_STR("L.has_key(key) -> Check if key is there in L")}, 611 | {"get", (PyCFunction)LRU_get, METH_VARARGS | METH_KEYWORDS, 612 | PyDoc_STR("L.get(key, default=None) -> If L has key return its value, otherwise default")}, 613 | {"setdefault", (PyCFunction)LRU_setdefault, METH_VARARGS, 614 | 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")}, 615 | {"pop", (PyCFunction)LRU_pop, METH_VARARGS | METH_KEYWORDS, 616 | 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.")}, 617 | {"popitem", (PyCFunction)LRU_popitem, METH_VARARGS | METH_KEYWORDS, 618 | 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.")}, 619 | {"set_size", (PyCFunction)LRU_set_size, METH_VARARGS, 620 | PyDoc_STR("L.set_size() -> set size of LRU")}, 621 | {"get_size", (PyCFunction)LRU_get_size, METH_NOARGS, 622 | PyDoc_STR("L.get_size() -> get size of LRU")}, 623 | {"clear", (PyCFunction)LRU_clear, METH_NOARGS, 624 | PyDoc_STR("L.clear() -> clear LRU")}, 625 | {"get_stats", (PyCFunction)LRU_get_stats, METH_NOARGS, 626 | PyDoc_STR("L.get_stats() -> returns a tuple with cache hits and misses")}, 627 | {"peek_first_item", (PyCFunction)LRU_peek_first_item, METH_NOARGS, 628 | PyDoc_STR("L.peek_first_item() -> returns the MRU item (key,value) without changing key order")}, 629 | {"peek_last_item", (PyCFunction)LRU_peek_last_item, METH_NOARGS, 630 | PyDoc_STR("L.peek_last_item() -> returns the LRU item (key,value) without changing key order")}, 631 | {"update", (PyCFunction)LRU_update, METH_VARARGS | METH_KEYWORDS, 632 | PyDoc_STR("L.update() -> update value for key in LRU")}, 633 | {"set_callback", (PyCFunction)LRU_set_callback, METH_VARARGS, 634 | PyDoc_STR("L.set_callback(callback) -> set a callback to call when an item is evicted.")}, 635 | {NULL, NULL}, 636 | }; 637 | 638 | static PyObject* 639 | LRU_repr(LRU* self) 640 | { 641 | return PyObject_Repr(self->dict); 642 | } 643 | 644 | static int 645 | LRU_init(LRU *self, PyObject *args, PyObject *kwds) 646 | { 647 | static char *kwlist[] = {"size", "callback", NULL}; 648 | PyObject *callback = NULL; 649 | self->callback = NULL; 650 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|O", kwlist, &self->size, &callback)) { 651 | return -1; 652 | } 653 | 654 | if (callback && callback != Py_None) { 655 | if (!PyCallable_Check(callback)) { 656 | PyErr_SetString(PyExc_TypeError, "parameter must be callable"); 657 | return -1; 658 | } 659 | Py_XINCREF(callback); 660 | self->callback = callback; 661 | } 662 | 663 | if ((Py_ssize_t)self->size <= 0) { 664 | PyErr_SetString(PyExc_ValueError, "Size should be a positive number"); 665 | return -1; 666 | } 667 | self->dict = PyDict_New(); 668 | self->first = self->last = NULL; 669 | self->hits = 0; 670 | self->misses = 0; 671 | return 0; 672 | } 673 | 674 | static void 675 | LRU_dealloc(LRU *self) 676 | { 677 | if (self->dict) { 678 | LRU_clear(self); 679 | Py_CLEAR(self->dict); 680 | Py_XDECREF(self->callback); 681 | } 682 | Py_TYPE(self)->tp_free((PyObject *)self); 683 | } 684 | 685 | PyDoc_STRVAR(lru_doc, 686 | "LRU(size, callback=None) -> new LRU dict that can store up to size elements\n" 687 | "An LRU dict behaves like a standard dict, except that it stores only fixed\n" 688 | "set of elements. Once the size overflows, it evicts least recently used\n" 689 | "items. If a callback is set it will call the callback with the evicted key\n" 690 | " and item.\n\n" 691 | "Eg:\n" 692 | ">>> l = LRU(3)\n" 693 | ">>> for i in range(5):\n" 694 | ">>> l[i] = str(i)\n" 695 | ">>> l.keys()\n" 696 | "[2,3,4]\n\n" 697 | "Note: An LRU(n) can be thought of as a dict that will have the most\n" 698 | "recently accessed n items.\n"); 699 | 700 | static PyTypeObject LRUType = { 701 | PyVarObject_HEAD_INIT(NULL, 0) 702 | "_lru.LRU", /* tp_name */ 703 | sizeof(LRU), /* tp_basicsize */ 704 | 0, /* tp_itemsize */ 705 | (destructor)LRU_dealloc, /* tp_dealloc */ 706 | 0, /* tp_print */ 707 | 0, /* tp_getattr */ 708 | 0, /* tp_setattr */ 709 | 0, /* tp_compare */ 710 | (reprfunc)LRU_repr, /* tp_repr */ 711 | 0, /* tp_as_number */ 712 | &lru_as_sequence, /* tp_as_sequence */ 713 | &LRU_as_mapping, /* tp_as_mapping */ 714 | 0, /* tp_hash */ 715 | 0, /* tp_call */ 716 | 0, /* tp_str */ 717 | 0, /* tp_getattro */ 718 | 0, /* tp_setattro */ 719 | 0, /* tp_as_buffer */ 720 | Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ 721 | lru_doc, /* tp_doc */ 722 | 0, /* tp_traverse */ 723 | 0, /* tp_clear */ 724 | 0, /* tp_richcompare */ 725 | 0, /* tp_weaklistoffset */ 726 | 0, /* tp_iter */ 727 | 0, /* tp_iternext */ 728 | LRU_methods, /* tp_methods */ 729 | 0, /* tp_members */ 730 | 0, /* tp_getset */ 731 | 0, /* tp_base */ 732 | 0, /* tp_dict */ 733 | 0, /* tp_descr_get */ 734 | 0, /* tp_descr_set */ 735 | 0, /* tp_dictoffset */ 736 | (initproc)LRU_init, /* tp_init */ 737 | 0, /* tp_alloc */ 738 | PyType_GenericNew, /* tp_new */ 739 | }; 740 | 741 | #if PY_MAJOR_VERSION >= 3 742 | static struct PyModuleDef moduledef = { 743 | PyModuleDef_HEAD_INIT, 744 | "_lru", /* m_name */ 745 | lru_doc, /* m_doc */ 746 | -1, /* m_size */ 747 | NULL, /* m_methods */ 748 | NULL, /* m_reload */ 749 | NULL, /* m_traverse */ 750 | NULL, /* m_clear */ 751 | NULL, /* m_free */ 752 | }; 753 | #endif 754 | 755 | static PyObject * 756 | moduleinit(void) 757 | { 758 | PyObject *m; 759 | 760 | LRUType.tp_base = &PyBaseObject_Type; 761 | 762 | if (PyType_Ready(&NodeType) < 0) 763 | return NULL; 764 | 765 | if (PyType_Ready(&LRUType) < 0) 766 | return NULL; 767 | 768 | #if PY_MAJOR_VERSION >= 3 769 | m = PyModule_Create(&moduledef); 770 | #else 771 | m = Py_InitModule3("_lru", NULL, lru_doc); 772 | #endif 773 | 774 | if (m == NULL) 775 | return NULL; 776 | 777 | Py_INCREF(&NodeType); 778 | Py_INCREF(&LRUType); 779 | PyModule_AddObject(m, "LRU", (PyObject *) &LRUType); 780 | 781 | return m; 782 | } 783 | 784 | #if PY_MAJOR_VERSION < 3 785 | PyMODINIT_FUNC 786 | init_lru(void) 787 | { 788 | moduleinit(); 789 | } 790 | #else 791 | PyMODINIT_FUNC 792 | PyInit__lru(void) 793 | { 794 | return moduleinit(); 795 | } 796 | #endif 797 | -------------------------------------------------------------------------------- /src/lru/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amitdev/lru-dict/fadf112a5060586160bc9a1db9f7673bee6da91d/src/lru/py.typed -------------------------------------------------------------------------------- /test/test_lru.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import random 3 | import sys 4 | import unittest 5 | from lru import LRU 6 | 7 | SIZES = [1, 2, 10, 1000] 8 | 9 | # Only available on debug python builds. 10 | gettotalrefcount = getattr(sys, 'gettotalrefcount', lambda: 0) 11 | 12 | 13 | class TestLRU(unittest.TestCase): 14 | 15 | def setUp(self): 16 | gc.collect() 17 | self._before_count = gettotalrefcount() 18 | 19 | def tearDown(self): 20 | after_count = gettotalrefcount() 21 | self.assertEqual(self._before_count, after_count) 22 | 23 | def _check_kvi(self, valid_keys, l): 24 | valid_keys = list(valid_keys) 25 | valid_vals = list(map(str, valid_keys)) 26 | self.assertEqual(valid_keys, l.keys()) 27 | self.assertEqual(valid_vals, l.values()) 28 | self.assertEqual(list(zip(valid_keys, valid_vals)), l.items()) 29 | 30 | def test_invalid_size(self): 31 | self.assertRaises(ValueError, LRU, -1) 32 | self.assertRaises(ValueError, LRU, 0) 33 | 34 | def test_empty(self): 35 | l = LRU(1) 36 | self.assertEqual([], l.keys()) 37 | self.assertEqual([], l.values()) 38 | 39 | def test_add_within_size(self): 40 | for size in SIZES: 41 | l = LRU(size) 42 | for i in range(size): 43 | l[i] = str(i) 44 | self._check_kvi(range(size - 1, -1, -1), l) 45 | 46 | def test_delete_multiple_within_size(self): 47 | for size in SIZES: 48 | l = LRU(size) 49 | for i in range(size): 50 | l[i] = str(i) 51 | for i in range(0, size, 2): 52 | del l[i] 53 | self._check_kvi(range(size - 1, 0, -2), l) 54 | for i in range(0, size, 2): 55 | with self.assertRaises(KeyError): 56 | l[i] 57 | 58 | def test_delete_multiple(self): 59 | for size in SIZES: 60 | l = LRU(size) 61 | n = size * 2 62 | for i in range(n): 63 | l[i] = str(i) 64 | for i in range(size, n, 2): 65 | del l[i] 66 | self._check_kvi(range(n - 1, size, -2), l) 67 | for i in range(0, size): 68 | with self.assertRaises(KeyError): 69 | l[i] 70 | for i in range(size, n, 2): 71 | with self.assertRaises(KeyError): 72 | l[i] 73 | 74 | def test_add_multiple(self): 75 | for size in SIZES: 76 | l = LRU(size) 77 | for i in range(size): 78 | l[i] = str(i) 79 | l[size] = str(size) 80 | self._check_kvi(range(size, 0, -1), l) 81 | 82 | def test_access_within_size(self): 83 | for size in SIZES: 84 | l = LRU(size) 85 | for i in range(size): 86 | l[i] = str(i) 87 | for i in range(size): 88 | self.assertEqual(l[i], str(i)) 89 | self.assertEqual(l.get(i, None), str(i)) 90 | 91 | def test_contains(self): 92 | for size in SIZES: 93 | l = LRU(size) 94 | for i in range(size): 95 | l[i] = str(i) 96 | for i in range(size): 97 | self.assertTrue(i in l) 98 | 99 | def test_access(self): 100 | for size in SIZES: 101 | l = LRU(size) 102 | n = size * 2 103 | for i in range(n): 104 | l[i] = str(i) 105 | self._check_kvi(range(n - 1, size - 1, -1), l) 106 | for i in range(size, n): 107 | self.assertEqual(l[i], str(i)) 108 | self.assertEqual(l.get(i, None), str(i)) 109 | 110 | def test_update(self): 111 | l = LRU(2) 112 | l['a'] = 1 113 | self.assertEqual(l['a'], 1) 114 | l.update(a=2) 115 | self.assertEqual(l['a'], 2) 116 | l['b'] = 2 117 | self.assertEqual(l['b'], 2) 118 | l.update(b=3) 119 | self.assertEqual(('b', 3), l.peek_first_item()) 120 | self.assertEqual(l['a'], 2) 121 | self.assertEqual(l['b'], 3) 122 | l.update({'a': 1, 'b': 2}) 123 | self.assertEqual(('b', 2), l.peek_first_item()) 124 | self.assertEqual(l['a'], 1) 125 | self.assertEqual(l['b'], 2) 126 | l.update() 127 | self.assertEqual(('b', 2), l.peek_first_item()) 128 | l.update(a=2) 129 | self.assertEqual(('a', 2), l.peek_first_item()) 130 | 131 | def test_peek_first_item(self): 132 | l = LRU(2) 133 | self.assertEqual(None, l.peek_first_item()) 134 | l[1] = '1' 135 | l[2] = '2' 136 | self.assertEqual((2, '2'), l.peek_first_item()) 137 | 138 | def test_peek_last_item(self): 139 | l = LRU(2) 140 | self.assertEqual(None, l.peek_last_item()) 141 | l[1] = '1' 142 | l[2] = '2' 143 | self.assertEqual((1, '1'), l.peek_last_item()) 144 | 145 | def test_overwrite(self): 146 | l = LRU(1) 147 | l[1] = '2' 148 | l[1] = '1' 149 | self.assertEqual('1', l[1]) 150 | self._check_kvi([1], l) 151 | 152 | def test_has_key(self): 153 | for size in SIZES: 154 | l = LRU(size) 155 | for i in range(2 * size): 156 | l[i] = str(i) 157 | self.assertTrue(l.has_key(i)) 158 | for i in range(size, 2 * size): 159 | self.assertTrue(l.has_key(i)) 160 | for i in range(size): 161 | self.assertFalse(l.has_key(i)) 162 | 163 | def test_capacity_get(self): 164 | for size in SIZES: 165 | l = LRU(size) 166 | self.assertTrue(size == l.get_size()) 167 | 168 | def test_capacity_set(self): 169 | for size in SIZES: 170 | l = LRU(size) 171 | for i in range(size + 5): 172 | l[i] = str(i) 173 | l.set_size(size + 10) 174 | self.assertTrue(size + 10 == l.get_size()) 175 | self.assertTrue(len(l) == size) 176 | for i in range(size + 20): 177 | l[i] = str(i) 178 | self.assertTrue(len(l) == size + 10) 179 | l.set_size(size + 10 - 1) 180 | self.assertTrue(len(l) == size + 10 - 1) 181 | 182 | def test_unhashable(self): 183 | l = LRU(1) 184 | self.assertRaises(TypeError, lambda: l[{'a': 'b'}]) 185 | with self.assertRaises(TypeError): 186 | l[['1']] = '2' 187 | with self.assertRaises(TypeError): 188 | del l[{'1': '1'}] 189 | 190 | def test_clear(self): 191 | for size in SIZES: 192 | l = LRU(size) 193 | for i in range(size + 5): 194 | l[i] = str(i) 195 | l.clear() 196 | for i in range(size): 197 | l[i] = str(i) 198 | for i in range(size): 199 | _ = l[random.randint(0, size - 1)] 200 | l.clear() 201 | self.assertTrue(len(l) == 0) 202 | 203 | def test_get_and_del(self): 204 | l = LRU(2) 205 | l[1] = '1' 206 | self.assertEqual('1', l.get(1)) 207 | self.assertEqual('1', l.get(2, '1')) 208 | self.assertIsNone(l.get(2)) 209 | self.assertEqual('1', l[1]) 210 | self.assertRaises(KeyError, lambda: l['2']) 211 | with self.assertRaises(KeyError): 212 | del l['2'] 213 | 214 | def test_setdefault(self): 215 | l = LRU(2) 216 | l[1] = '1' 217 | val = l.setdefault(1) 218 | self.assertEqual('1', val) 219 | self.assertEqual((1, 0), l.get_stats()) 220 | val = l.setdefault(2, '2') 221 | self.assertEqual('2', val) 222 | self.assertEqual((1, 1), l.get_stats()) 223 | self.assertEqual(val, l[2]) 224 | l.clear() 225 | val = 'long string' * 512 226 | l.setdefault(1, val) 227 | l[2] = '2' 228 | l[3] = '3' 229 | self.assertTrue(val) 230 | 231 | def test_pop(self): 232 | l = LRU(2) 233 | v = '2' * 4096 234 | l[1] = '1' 235 | l[2] = v 236 | val = l.pop(1) 237 | self.assertEqual('1', val) 238 | self.assertEqual((1, 0), l.get_stats()) 239 | val = l.pop(2, 'not used') 240 | self.assertEqual(v, val) 241 | del val 242 | self.assertTrue(v) 243 | self.assertEqual((2, 0), l.get_stats()) 244 | val = l.pop(3, '3' * 4096) 245 | self.assertEqual('3' * 4096, val) 246 | self.assertEqual((2, 1), l.get_stats()) 247 | self.assertEqual(0, len(l)) 248 | with self.assertRaises(KeyError) as ke: 249 | l.pop(4) 250 | self.assertEqual(4, ke.args[0]) # type: ignore 251 | self.assertEqual((2, 2), l.get_stats()) 252 | self.assertEqual(0, len(l)) 253 | with self.assertRaises(TypeError): 254 | l.pop() # type: ignore 255 | 256 | def test_popitem(self): 257 | l = LRU(3) 258 | l[1] = '1' 259 | l[2] = '2' 260 | l[3] = '3' 261 | k, v = l.popitem() 262 | self.assertEqual((1, '1'), (k, v)) 263 | k, v = l.popitem(least_recent=False) 264 | self.assertEqual((3, '3'), (k, v)) 265 | self.assertEqual((2, '2'), l.popitem(True)) 266 | with self.assertRaises(KeyError) as ke: 267 | l.popitem() 268 | self.assertEqual('popitem(): LRU dict is empty', ke.args[0]) # type: ignore 269 | self.assertEqual((0, 0), l.get_stats()) 270 | 271 | def test_stats(self): 272 | for size in SIZES: 273 | l = LRU(size) 274 | for i in range(size): 275 | l[i] = str(i) 276 | 277 | self.assertTrue(l.get_stats() == (0, 0)) 278 | 279 | val = l[0] 280 | self.assertTrue(l.get_stats() == (1, 0)) 281 | 282 | val = l.get(0, None) 283 | self.assertTrue(l.get_stats() == (2, 0)) 284 | 285 | val = l.get(-1, None) 286 | self.assertTrue(l.get_stats() == (2, 1)) 287 | 288 | try: 289 | val = l[-1] 290 | except: 291 | pass 292 | 293 | self.assertTrue(l.get_stats() == (2, 2)) 294 | 295 | l.clear() 296 | self.assertTrue(len(l) == 0) 297 | self.assertTrue(l.get_stats() == (0, 0)) 298 | 299 | def test_lru(self): 300 | l = LRU(1) 301 | l['a'] = 1 302 | l['a'] 303 | self.assertEqual(l.keys(), ['a']) 304 | l['b'] = 2 305 | self.assertEqual(l.keys(), ['b']) 306 | 307 | l = LRU(2) 308 | l['a'] = 1 309 | l['b'] = 2 310 | self.assertEqual(len(l), 2) 311 | l['a'] # Testing the first one 312 | l['c'] = 3 313 | self.assertEqual(sorted(l.keys()), ['a', 'c']) 314 | l['c'] 315 | self.assertEqual(sorted(l.keys()), ['a', 'c']) 316 | 317 | l = LRU(3) 318 | l['a'] = 1 319 | l['b'] = 2 320 | l['c'] = 3 321 | self.assertEqual(len(l), 3) 322 | l['b'] # Testing the middle one 323 | l['d'] = 4 324 | self.assertEqual(sorted(l.keys()), ['b', 'c', 'd']) 325 | l['d'] # Testing the last one 326 | self.assertEqual(sorted(l.keys()), ['b', 'c', 'd']) 327 | l['e'] = 5 328 | self.assertEqual(sorted(l.keys()), ['b', 'd', 'e']) 329 | 330 | def test_callback(self): 331 | 332 | counter = [0] 333 | 334 | first_key = 'a' 335 | first_value = 1 336 | 337 | def callback(key, value): 338 | self.assertEqual(key, first_key) 339 | self.assertEqual(value, first_value) 340 | counter[0] += 1 341 | 342 | l = LRU(1, callback=callback) 343 | l[first_key] = first_value 344 | l['b'] = 1 # test calling the callback 345 | 346 | self.assertEqual(counter[0], 1) 347 | self.assertEqual(l.keys(), ['b']) 348 | 349 | l['b'] = 2 # doesn't call callback 350 | self.assertEqual(counter[0], 1) 351 | self.assertEqual(l.keys(), ['b']) 352 | self.assertEqual(l.values(), [2]) 353 | 354 | l = LRU(1, callback=callback) 355 | l[first_key] = first_value 356 | 357 | l.set_callback(None) 358 | l['c'] = 1 # doesn't call callback 359 | self.assertEqual(counter[0], 1) 360 | self.assertEqual(l.keys(), ['c']) 361 | 362 | l.set_callback(callback) 363 | del l['c'] # doesn't call callback 364 | self.assertEqual(counter[0], 1) 365 | self.assertEqual(l.keys(), []) 366 | 367 | l = LRU(2, callback=callback) 368 | l['a'] = 1 # test calling the callback 369 | l['b'] = 2 # test calling the callback 370 | 371 | self.assertEqual(counter[0], 1) 372 | self.assertEqual(l.keys(), ['b', 'a']) 373 | l.set_size(1) 374 | self.assertEqual(counter[0], 2) # callback invoked 375 | self.assertEqual(l.keys(), ['b']) 376 | 377 | def test_subclassing(self): 378 | class LRUChild(LRU): 379 | ... 380 | 381 | l = LRUChild(1) 382 | l['a'] = 1 383 | self.assertEqual(l['a'], 1) 384 | self.assertEqual(l.keys(), ['a']) 385 | self.assertEqual(l.values(), [1]) 386 | 387 | 388 | if __name__ == '__main__': 389 | unittest.main() 390 | --------------------------------------------------------------------------------