Skip to content

Commit a3ac26f

Browse files
committed
Implement Mapping for GroupingLatestValueCache
Signed-off-by: Sahas Subramanian <[email protected]>
1 parent 468500c commit a3ac26f

File tree

2 files changed

+109
-41
lines changed

2 files changed

+109
-41
lines changed

src/frequenz/channels/experimental/_grouping_latest_value_cache.py

Lines changed: 99 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
takes a [Receiver][frequenz.channels.Receiver] and a `key` function as arguments and
1111
stores the latest value received by that receiver for each key separately.
1212
13-
As soon as a value is received for a `key`, the
14-
[`has_value`][frequenz.channels.experimental.GroupingLatestValueCache.has_value] method
15-
returns `True` for that `key`, and the [`get`][frequenz.channels.LatestValueCache.get]
16-
method for that `key` returns the latest value received. The `get` method will raise an
17-
exception if called before any messages have been received from the receiver for a given
18-
`key`.
13+
The `GroupingLatestValueCache` implements the [`Mapping`][collections.abc.Mapping]
14+
interface, so it can be used like a dictionary. In addition, it provides a
15+
[has_value][frequenz.channels.experimental.GroupingLatestValueCache.has_value] method to
16+
check if a value has been received for a specific key, and a
17+
[clear][frequenz.channels.experimental.GroupingLatestValueCache.clear] method to clear
18+
the cached value for a specific key.
1919
2020
Example:
2121
```python
@@ -39,26 +39,25 @@
3939

4040
import asyncio
4141
import typing
42-
from collections.abc import Set
42+
from collections.abc import ItemsView, Iterator, KeysView, Mapping, ValuesView
43+
44+
from typing_extensions import override
4345

4446
from .._receiver import Receiver
4547

4648
T_co = typing.TypeVar("T_co", covariant=True)
49+
T = typing.TypeVar("T")
4750
HashableT = typing.TypeVar("HashableT", bound=typing.Hashable)
4851

4952

50-
class GroupingLatestValueCache(typing.Generic[T_co, HashableT]):
51-
"""A cache that stores the latest value in a receiver.
52-
53-
It provides a way to look up the latest value in a stream without any delay,
54-
as long as there has been one value received.
55-
"""
53+
class GroupingLatestValueCache(Mapping[HashableT, T_co]):
54+
"""A cache that stores the latest value in a receiver, grouped by key."""
5655

5756
def __init__(
5857
self,
5958
receiver: Receiver[T_co],
60-
key: typing.Callable[[T_co], typing.Any],
6159
*,
60+
key: typing.Callable[[T_co], HashableT],
6261
unique_id: str | None = None,
6362
) -> None:
6463
"""Create a new cache.
@@ -84,47 +83,119 @@ def unique_id(self) -> str:
8483
"""The unique identifier of this instance."""
8584
return self._unique_id
8685

87-
def keys(self) -> Set[HashableT]:
86+
@override
87+
def keys(self) -> KeysView[HashableT]:
8888
"""Return the set of keys for which values have been received.
8989
9090
If no key function is provided, this will return an empty set.
9191
"""
9292
return self._latest_value_by_key.keys()
9393

94-
def get(self, key: HashableT) -> T_co:
94+
@override
95+
def items(self) -> ItemsView[HashableT, T_co]:
96+
"""Return an iterator over the key-value pairs of the latest values received."""
97+
return self._latest_value_by_key.items()
98+
99+
@override
100+
def values(self) -> ValuesView[T_co]:
101+
"""Return an iterator over the latest values received."""
102+
return self._latest_value_by_key.values()
103+
104+
@typing.overload
105+
def get(self, key: HashableT, default: None = None) -> T_co | None:
106+
"""Return the latest value that has been received for a specific key."""
107+
108+
# MyPy passes this overload as a valid signature, but pylint does not like it.
109+
@typing.overload
110+
def get( # pylint: disable=signature-differs
111+
self, key: HashableT, default: T
112+
) -> T_co | T:
113+
"""Return the latest value that has been received for a specific key."""
114+
115+
@override
116+
def get(self, key: HashableT, default: T | None = None) -> T_co | T | None:
95117
"""Return the latest value that has been received.
96118
97-
This raises a `ValueError` if no value has been received yet. Use `has_value` to
98-
check whether a value has been received yet, before trying to access the value,
99-
to avoid the exception.
100-
101119
Args:
102120
key: An optional key to retrieve the latest value for that key. If not
103121
provided, it retrieves the latest value received overall.
122+
default: The default value to return if no value has been received yet for
123+
the specified key. If not provided, it defaults to `None`.
104124
105125
Returns:
106126
The latest value that has been received.
127+
"""
128+
return self._latest_value_by_key.get(key, default)
129+
130+
@override
131+
def __iter__(self) -> Iterator[HashableT]:
132+
"""Return an iterator over the keys for which values have been received."""
133+
return iter(self._latest_value_by_key)
134+
135+
@override
136+
def __len__(self) -> int:
137+
"""Return the number of keys for which values have been received."""
138+
return len(self._latest_value_by_key)
139+
140+
@override
141+
def __getitem__(self, key: HashableT) -> T_co:
142+
"""Return the latest value that has been received for a specific key.
143+
144+
Args:
145+
key: The key to retrieve the latest value for.
146+
147+
Returns:
148+
The latest value that has been received for that key.
107149
108150
Raises:
109-
ValueError: If no value has been received yet.
151+
KeyError: If no value has been received yet for that key.
110152
"""
111153
if key not in self._latest_value_by_key:
112-
raise ValueError(f"No value received for key: {key!r}")
154+
raise KeyError(f"No value received for key: {key!r}")
113155
return self._latest_value_by_key[key]
114156

115-
def has_value(self, key: HashableT) -> bool:
116-
"""Check whether a value has been received yet.
117-
118-
If `key` is provided, it checks whether a value has been received for that key.
157+
@override
158+
def __contains__(self, key: object, /) -> bool:
159+
"""Check if a value has been received for a specific key.
119160
120161
Args:
121-
key: An optional key to check if a value has been received for that key.
162+
key: The key to check for.
122163
123164
Returns:
124-
`True` if a value has been received, `False` otherwise.
165+
`True` if a value has been received for that key, `False` otherwise.
125166
"""
126167
return key in self._latest_value_by_key
127168

169+
@override
170+
def __eq__(self, other: object, /) -> bool:
171+
"""Check if this cache is equal to another object.
172+
173+
Two caches are considered equal if they have the same keys and values.
174+
175+
Args:
176+
other: The object to compare with.
177+
178+
Returns:
179+
`True` if the caches are equal, `False` otherwise.
180+
"""
181+
if not isinstance(other, GroupingLatestValueCache):
182+
return NotImplemented
183+
return self._latest_value_by_key == other._latest_value_by_key
184+
185+
@override
186+
def __ne__(self, value: object, /) -> bool:
187+
"""Check if this cache is not equal to another object.
188+
189+
Args:
190+
value: The object to compare with.
191+
192+
Returns:
193+
`True` if the caches are not equal, `False` otherwise.
194+
"""
195+
if not isinstance(value, GroupingLatestValueCache):
196+
return NotImplemented
197+
return self._latest_value_by_key != value._latest_value_by_key
198+
128199
def clear(self, key: HashableT) -> None:
129200
"""Clear the latest value for a specific key.
130201

tests/test_grouping_latest_value_cache_integration.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@ async def test_latest_value_cache_key() -> None:
1616
"""Ensure LatestValueCache works with keys."""
1717
channel = Broadcast[tuple[int, str]](name="lvc_test")
1818

19-
cache: GroupingLatestValueCache[tuple[int, str], int] = GroupingLatestValueCache(
19+
cache: GroupingLatestValueCache[int, tuple[int, str]] = GroupingLatestValueCache(
2020
channel.new_receiver(), key=lambda x: x[0]
2121
)
2222
sender = channel.new_sender()
2323

24-
assert not cache.has_value(5)
25-
with pytest.raises(ValueError, match="No value received for key: 0"):
26-
cache.get(0)
24+
assert 5 not in cache
25+
assert cache.get(0) is None
2726

2827
assert cache.keys() == set()
2928

@@ -32,17 +31,16 @@ async def test_latest_value_cache_key() -> None:
3231
await sender.send((5, "c"))
3332
await asyncio.sleep(0)
3433

35-
assert cache.has_value(5)
36-
assert cache.has_value(6)
37-
assert not cache.has_value(7)
34+
assert 5 in cache
35+
assert 6 in cache
36+
assert 7 not in cache
3837

3938
assert cache.get(5) == (5, "c")
4039
assert cache.get(6) == (6, "b")
4140

4241
assert cache.keys() == {5, 6}
4342

44-
with pytest.raises(ValueError, match="No value received for key: 7"):
45-
cache.get(7)
43+
assert cache.get(7, default=(7, "default")) == (7, "default")
4644

4745
await sender.send((12, "d"))
4846
await asyncio.sleep(0)
@@ -62,9 +60,8 @@ async def test_latest_value_cache_key() -> None:
6260
assert cache.keys() == {5, 6, 12}
6361

6462
cache.clear(5)
65-
assert not cache.has_value(5)
66-
assert cache.has_value(6)
63+
assert 5 not in cache
64+
assert 6 in cache
6765

68-
with pytest.raises(ValueError, match="No value received for key: 5"):
69-
assert cache.get(5)
66+
assert cache.get(5) is None
7067
assert cache.keys() == {6, 12}

0 commit comments

Comments
 (0)