Skip to content

Commit fcd4085

Browse files
committed
Add other MutableMapping methods to clear items
We can offer all item-clearing methods from `MutableMapping`, we just don't want to allow users to update values. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent a5cbad3 commit fcd4085

File tree

2 files changed

+119
-6
lines changed

2 files changed

+119
-6
lines changed

src/frequenz/channels/experimental/_grouping_latest_value_cache.py

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
"""Type variable for the keys used to group values in the `GroupingLatestValueCache`."""
2323

2424

25+
class _NotSpecified:
26+
"""A sentinel value to indicate that no default value was provided."""
27+
28+
def __repr__(self) -> str:
29+
"""Return a string representation of this sentinel."""
30+
return "<_NotSpecified>"
31+
32+
2533
class GroupingLatestValueCache(Mapping[HashableT, ValueT_co]):
2634
"""A cache that stores the latest value in a receiver, grouped by key.
2735
@@ -33,12 +41,15 @@ class GroupingLatestValueCache(Mapping[HashableT, ValueT_co]):
3341
stores the latest value received by that receiver for each key separately.
3442
3543
The `GroupingLatestValueCache` implements the [`Mapping`][collections.abc.Mapping]
36-
interface, so it can be used like a dictionary. It is not
37-
a [`MutableMapping`][collections.abc.MutableMapping] because users can't mutate the
38-
cache directly, it is only mutated by the underlying receiver. There is one exception
39-
though, users can clear individual keys from the cache using the
40-
[__delitem__][frequenz.channels.experimental.GroupingLatestValueCache.__delitem__]
41-
method.
44+
interface, so it can be used like a dictionary. Additionally other methods from
45+
[`MutableMapping`][collections.abc.MutableMapping] are implemented, but only
46+
methods removing items from the cache are allowed, such as
47+
[`pop()`][frequenz.channels.experimental.GroupingLatestValueCache.pop],
48+
[`popitem()`][frequenz.channels.experimental.GroupingLatestValueCache.popitem],
49+
[`clear()`][frequenz.channels.experimental.GroupingLatestValueCache.clear], and
50+
[`__delitem__()`][frequenz.channels.experimental.GroupingLatestValueCache.__delitem__].
51+
Other update methods are not provided because the user should not update the
52+
cache values directly.
4253
4354
Example:
4455
```python
@@ -220,6 +231,50 @@ def __delitem__(self, key: HashableT) -> None:
220231
"""
221232
del self._latest_value_by_key[key]
222233

234+
@typing.overload
235+
def pop(self, key: HashableT, /) -> ValueT_co | None:
236+
"""Remove the latest value for a specific key and return it."""
237+
238+
@typing.overload
239+
def pop(self, key: HashableT, /, default: DefaultT) -> ValueT_co | DefaultT:
240+
"""Remove the latest value for a specific key and return it."""
241+
242+
def pop(
243+
self, key: HashableT, /, default: DefaultT | _NotSpecified = _NotSpecified()
244+
) -> ValueT_co | DefaultT | None:
245+
"""Remove the latest value for a specific key and return it.
246+
247+
If no value has been received yet for that key, it returns the default value or
248+
raises a `KeyError` if no default value is provided.
249+
250+
Args:
251+
key: The key for which to remove the latest value.
252+
default: The default value to return if no value has been received yet for
253+
the specified key.
254+
255+
Returns:
256+
The latest value that has been received for that key, or the default value if
257+
no value has been received yet and a default value is provided.
258+
"""
259+
if isinstance(default, _NotSpecified):
260+
return self._latest_value_by_key.pop(key)
261+
return self._latest_value_by_key.pop(key, default)
262+
263+
def popitem(self) -> tuple[HashableT, ValueT_co]:
264+
"""Remove and return a (key, value) pair from the cache.
265+
266+
Pairs are returned in LIFO (last-in, first-out) order.
267+
268+
Returns:
269+
A tuple containing the key and the latest value that has been received for
270+
that key.
271+
"""
272+
return self._latest_value_by_key.popitem()
273+
274+
def clear(self) -> None:
275+
"""Clear all entries from the cache."""
276+
self._latest_value_by_key.clear()
277+
223278
async def stop(self) -> None:
224279
"""Stop the cache."""
225280
if not self._task.done():

tests/test_grouping_latest_value_cache_integration.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""Tests for the LatestValueCache implementation."""
55

66
import asyncio
7+
from math import exp
78

89
import pytest
910

@@ -36,7 +37,9 @@ async def test_latest_value_cache_key() -> None:
3637
assert 7 not in cache
3738

3839
assert cache.get(5) == (5, "c")
40+
assert cache[5] == (5, "c")
3941
assert cache.get(6) == (6, "b")
42+
assert cache[6] == (6, "b")
4043

4144
assert cache.keys() == {5, 6}
4245

@@ -65,3 +68,58 @@ async def test_latest_value_cache_key() -> None:
6568

6669
assert cache.get(5) is None
6770
assert cache.keys() == {6, 12}
71+
72+
assert cache.pop(6) == (6, "g")
73+
assert 6 not in cache
74+
assert cache.keys() == {12}
75+
76+
assert cache.pop(8, default=True) is True
77+
with pytest.raises(KeyError):
78+
cache.pop(8)
79+
80+
assert cache.popitem() == (12, (12, "d"))
81+
assert 12 not in cache
82+
assert not cache
83+
84+
await sender.send((1, "h"))
85+
await sender.send((2, "i"))
86+
await asyncio.sleep(0)
87+
88+
expected = {1: (1, "h"), 2: (2, "i")}
89+
assert cache.keys() == expected.keys()
90+
assert list(cache.values()) == list(expected.values())
91+
assert list(cache.items()) == list(expected.items())
92+
# assert cache == expected
93+
assert list(cache) == list(expected)
94+
95+
cache.clear()
96+
assert not cache
97+
assert cache.keys() == set()
98+
99+
await cache.stop()
100+
101+
102+
@pytest.mark.integration
103+
async def test_equality() -> None:
104+
"""Test that two caches with the same content are equal."""
105+
channel = Broadcast[tuple[int, str]](name="lvc_test")
106+
107+
cache1: GroupingLatestValueCache[int, tuple[int, str]] = GroupingLatestValueCache(
108+
channel.new_receiver(), key=lambda x: x[0]
109+
)
110+
cache2: GroupingLatestValueCache[int, tuple[int, str]] = GroupingLatestValueCache(
111+
channel.new_receiver(), key=lambda x: x[0]
112+
)
113+
114+
sender = channel.new_sender()
115+
await sender.send((1, "one"))
116+
await sender.send((2, "two"))
117+
await asyncio.sleep(0)
118+
119+
assert cache1 == cache2
120+
121+
del cache1[1]
122+
assert cache1 != cache2
123+
124+
await cache1.stop()
125+
await cache2.stop()

0 commit comments

Comments
 (0)