Skip to content

Commit 3ac8377

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 3ac8377

File tree

2 files changed

+119
-7
lines changed

2 files changed

+119
-7
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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313

1414
@pytest.mark.integration
15-
async def test_latest_value_cache_key() -> None:
15+
async def test_latest_value_cache_key() -> None: # pylint: disable=too-many-statements
1616
"""Ensure LatestValueCache works with keys."""
1717
channel = Broadcast[tuple[int, str]](name="lvc_test")
1818

@@ -36,7 +36,9 @@ async def test_latest_value_cache_key() -> None:
3636
assert 7 not in cache
3737

3838
assert cache.get(5) == (5, "c")
39+
assert cache[5] == (5, "c")
3940
assert cache.get(6) == (6, "b")
41+
assert cache[6] == (6, "b")
4042

4143
assert cache.keys() == {5, 6}
4244

@@ -65,3 +67,58 @@ async def test_latest_value_cache_key() -> None:
6567

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

0 commit comments

Comments
 (0)