Skip to content

Commit 00b5f6f

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

File tree

2 files changed

+110
-40
lines changed

2 files changed

+110
-40
lines changed

src/frequenz/channels/experimental/_grouping_latest_value_cache.py

Lines changed: 101 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,25 +39,26 @@
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(
54+
typing.Generic[T_co, HashableT], Mapping[HashableT, T_co]
55+
):
56+
"""A cache that stores the latest value in a receiver, grouped by key."""
5657

5758
def __init__(
5859
self,
5960
receiver: Receiver[T_co],
60-
key: typing.Callable[[T_co], typing.Any],
61+
key: typing.Callable[[T_co], HashableT],
6162
*,
6263
unique_id: str | None = None,
6364
) -> None:
@@ -84,47 +85,119 @@ def unique_id(self) -> str:
8485
"""The unique identifier of this instance."""
8586
return self._unique_id
8687

87-
def keys(self) -> Set[HashableT]:
88+
@override
89+
def keys(self) -> KeysView[HashableT]:
8890
"""Return the set of keys for which values have been received.
8991
9092
If no key function is provided, this will return an empty set.
9193
"""
9294
return self._latest_value_by_key.keys()
9395

94-
def get(self, key: HashableT) -> T_co:
96+
@override
97+
def items(self) -> ItemsView[HashableT, T_co]:
98+
"""Return an iterator over the key-value pairs of the latest values received."""
99+
return self._latest_value_by_key.items()
100+
101+
@override
102+
def values(self) -> ValuesView[T_co]:
103+
"""Return an iterator over the latest values received."""
104+
return self._latest_value_by_key.values()
105+
106+
@typing.overload
107+
def get(self, key: HashableT, default: None = None) -> T_co | None:
108+
"""Return the latest value that has been received for a specific key."""
109+
110+
# MyPy passes this overload as a valid signature, but pylint does not like it.
111+
@typing.overload
112+
def get( # pylint: disable=signature-differs
113+
self, key: HashableT, default: T
114+
) -> T_co | T:
115+
"""Return the latest value that has been received for a specific key."""
116+
117+
@override
118+
def get(self, key: HashableT, default: T | None = None) -> T_co | T | None:
95119
"""Return the latest value that has been received.
96120
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-
101121
Args:
102122
key: An optional key to retrieve the latest value for that key. If not
103123
provided, it retrieves the latest value received overall.
124+
default: The default value to return if no value has been received yet for
125+
the specified key. If not provided, it defaults to `None`.
104126
105127
Returns:
106128
The latest value that has been received.
129+
"""
130+
return self._latest_value_by_key.get(key, default)
131+
132+
@override
133+
def __iter__(self) -> Iterator[HashableT]:
134+
"""Return an iterator over the keys for which values have been received."""
135+
return iter(self._latest_value_by_key)
136+
137+
@override
138+
def __len__(self) -> int:
139+
"""Return the number of keys for which values have been received."""
140+
return len(self._latest_value_by_key)
141+
142+
@override
143+
def __getitem__(self, key: HashableT) -> T_co:
144+
"""Return the latest value that has been received for a specific key.
145+
146+
Args:
147+
key: The key to retrieve the latest value for.
148+
149+
Returns:
150+
The latest value that has been received for that key.
107151
108152
Raises:
109-
ValueError: If no value has been received yet.
153+
KeyError: If no value has been received yet for that key.
110154
"""
111155
if key not in self._latest_value_by_key:
112-
raise ValueError(f"No value received for key: {key!r}")
156+
raise KeyError(f"No value received for key: {key!r}")
113157
return self._latest_value_by_key[key]
114158

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.
159+
@override
160+
def __contains__(self, key: object, /) -> bool:
161+
"""Check if a value has been received for a specific key.
119162
120163
Args:
121-
key: An optional key to check if a value has been received for that key.
164+
key: The key to check for.
122165
123166
Returns:
124-
`True` if a value has been received, `False` otherwise.
167+
`True` if a value has been received for that key, `False` otherwise.
125168
"""
126169
return key in self._latest_value_by_key
127170

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

tests/test_grouping_latest_value_cache_integration.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ async def test_latest_value_cache_key() -> None:
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)