Skip to content

Commit 520f470

Browse files
committed
feat(lru_queue): add LRUQueue
1 parent a56bc67 commit 520f470

File tree

4 files changed

+170
-2
lines changed

4 files changed

+170
-2
lines changed

class_cache/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ def __init__(
1414
self,
1515
id_: IdType = None,
1616
backend_type: type[CacheInterface] | Callable[[IdType], CacheInterface] = DEFAULT_BACKEND_TYPE,
17+
max_items=128,
1718
) -> None:
1819
super().__init__(id_)
1920
self._backend = backend_type(id_)
2021
# TODO: Implement max_size logic
2122
self._data: dict[KeyType, ValueType] = {}
2223
self._to_write = set()
2324
self._to_delete = set()
25+
self._max_items = max_items
2426

2527
@property
2628
def backend(self) -> CacheInterface:

class_cache/lru_queue.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import Generic, Iterator
5+
6+
from .types import KeyType
7+
8+
9+
@dataclass(slots=True, eq=False)
10+
class Link(Generic[KeyType]):
11+
prev: Link
12+
next: Link
13+
key: KeyType
14+
15+
16+
# Adapted from https://github.com/python/cpython/blob/5592399313c963c110280a7c98de974889e1d353/Lib/functools.py#L542
17+
class LRUQueue(Generic[KeyType]):
18+
def __init__(self):
19+
self._links = {}
20+
self._root = Link(None, None, None) # type: ignore
21+
self._root.prev = self._root
22+
self._root.next = self._root
23+
24+
def __contains__(self, key: KeyType) -> bool:
25+
if result := key in self._links:
26+
self.update(key)
27+
return result
28+
29+
def _move_to_front(self, link: Link) -> None:
30+
next_ = self._root.next
31+
32+
next_.prev = link
33+
link.next = next_
34+
35+
self._root.next = link
36+
link.prev = self._root
37+
38+
@staticmethod
39+
def _connect_neighbours(link: Link) -> None:
40+
link.prev.next = link.next
41+
link.next.prev = link.prev
42+
43+
def update(self, key: KeyType) -> None:
44+
if key in self._links:
45+
link = self._links[key]
46+
# Connect neighbouring keys
47+
self._connect_neighbours(link)
48+
else:
49+
link = Link(None, None, key) # type: ignore
50+
self._links[key] = link
51+
self._move_to_front(link)
52+
53+
def peek(self) -> KeyType:
54+
if self._root.prev == self._root:
55+
raise IndexError("peeking into an empty queue")
56+
57+
return self._root.prev.key
58+
59+
def pop(self) -> KeyType:
60+
if self._root.prev == self._root:
61+
raise IndexError("pop from an empty queue")
62+
63+
last = self._root.prev
64+
self._connect_neighbours(last)
65+
key = last.key
66+
del self._links[key]
67+
return key
68+
69+
def __delitem__(self, key: KeyType) -> None:
70+
link = self._links[key]
71+
del self._links[key]
72+
self._connect_neighbours(link)
73+
74+
def __len__(self) -> int:
75+
return len(self._links)
76+
77+
def __iter__(self) -> Iterator[KeyType]:
78+
current = self._root
79+
while current.next != self._root:
80+
yield current.next.key
81+
current = current.next
82+
83+
def __str__(self) -> str:
84+
result = ""
85+
for key in self:
86+
result += f"{key} -> "
87+
return result[:-4]
88+
89+
90+
__all__ = ["LRUQueue"]

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ dependencies = ["Pympler", "brotli", "marisa-trie", "replete>=2.3.1"]
1515
Home = "https://github.com/Rizhiy/class-cache"
1616

1717
[project.optional-dependencies]
18-
test = ["pytest", "pytest-cov", "replete[testing]"]
18+
test = ["flaky", "pytest", "pytest-cov", "replete[testing]"]
1919
dev = ["black", "class-cache[test]", "numpy", "pre-commit", "ruff"]
2020

2121
[tool.flit.sdist]
@@ -27,7 +27,7 @@ version_variables = ["class_cache/__init__.py:__version__"]
2727

2828
[tool.pytest.ini_options]
2929
minversion = "6.0"
30-
addopts = "--doctest-modules --ignore=benchmark"
30+
addopts = "--doctest-modules --ignore=benchmark --no-flaky-report"
3131

3232
[tool.black]
3333
line-length = 120

tests/test_lru_queue.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from flaky import flaky
2+
from replete import Timer
3+
4+
from class_cache.lru_queue import LRUQueue
5+
6+
NUM_ITERS = 10_000
7+
8+
9+
def get_queue(size=4) -> LRUQueue:
10+
queue = LRUQueue()
11+
for i in range(size):
12+
queue.update(i)
13+
return queue
14+
15+
16+
def test_basic_queue():
17+
queue = get_queue()
18+
19+
assert len(queue) == 4
20+
for i in range(4):
21+
assert i in queue
22+
assert queue.peek() == 0
23+
queue.update(0)
24+
assert queue.peek() == 1
25+
assert queue.pop() == 1
26+
assert queue.peek() == 2
27+
del queue[2]
28+
assert len(queue) == 2
29+
assert queue.peek() == 3
30+
assert list(queue) == [0, 3]
31+
32+
33+
def test_next():
34+
queue = get_queue()
35+
assert next(queue) == 3 # type: ignore FP
36+
37+
38+
def test_str():
39+
queue = get_queue()
40+
queue.update(0)
41+
42+
assert str(queue) == "0 -> 3 -> 2 -> 1"
43+
44+
45+
@flaky(max_runs=3, min_passes=1) # This is very noisy
46+
def test_contains_speed():
47+
small_queue = get_queue()
48+
with Timer(process_only=True) as base_timer:
49+
for _ in range(NUM_ITERS):
50+
assert 0 in small_queue
51+
52+
large_queue = get_queue(1024)
53+
with Timer(base_timer.time, process_only=True) as timer:
54+
for _ in range(NUM_ITERS):
55+
assert 0 in large_queue
56+
57+
# Some noise is allowed
58+
assert timer.base_time_ratio < 1.05
59+
60+
61+
@flaky(max_runs=3, min_passes=1) # This is very noisy
62+
def test_pop_update_speed():
63+
small_queue = get_queue()
64+
with Timer(process_only=True) as base_timer:
65+
for _ in range(NUM_ITERS):
66+
key = small_queue.pop()
67+
small_queue.update(key)
68+
69+
large_queue = get_queue(1024)
70+
with Timer(base_timer.time, process_only=True) as timer:
71+
for _ in range(NUM_ITERS):
72+
key = large_queue.pop()
73+
large_queue.update(key)
74+
75+
# Some noise is allowed
76+
assert timer.base_time_ratio < 1.05

0 commit comments

Comments
 (0)