Skip to content

Commit 59b1184

Browse files
committed
Add support for skipping None configs
This is useful for cases where the the receiver can't react to `None` configurations, either because it is handled externally or because it should just keep the previous configuration. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent e7aa645 commit 59b1184

File tree

1 file changed

+67
-5
lines changed

1 file changed

+67
-5
lines changed

src/frequenz/sdk/config/_manager.py

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pathlib
99
from collections.abc import Mapping, Sequence
1010
from datetime import timedelta
11-
from typing import Any, Final, TypeGuard, assert_type, overload
11+
from typing import Any, Final, Literal, TypeGuard, assert_type, cast, overload
1212

1313
from frequenz.channels import Broadcast, Receiver
1414
from frequenz.channels.experimental import WithPrevious
@@ -106,6 +106,7 @@ async def new_receiver(
106106
*,
107107
wait_for_first: bool = True,
108108
skip_unchanged: bool = True,
109+
skip_none: Literal[False] = False,
109110
) -> Receiver[Mapping[str, Any]]: ...
110111

111112
@overload
@@ -114,6 +115,7 @@ async def new_receiver( # pylint: disable=too-many-arguments
114115
*,
115116
wait_for_first: bool = True,
116117
skip_unchanged: bool = True,
118+
skip_none: Literal[False] = False,
117119
# We need to specify the key here because we have kwargs, so if it is not
118120
# present is not considered None as the only possible value, as any value can be
119121
# accepted as part of the kwargs.
@@ -129,27 +131,54 @@ async def new_receiver(
129131
*,
130132
wait_for_first: bool = True,
131133
skip_unchanged: bool = True,
134+
skip_none: Literal[False] = False,
132135
key: str | Sequence[str],
133136
) -> Receiver[Mapping[str, Any] | None]: ...
134137

138+
@overload
139+
async def new_receiver(
140+
self,
141+
*,
142+
wait_for_first: bool = True,
143+
skip_unchanged: bool = True,
144+
skip_none: Literal[True] = True,
145+
key: str | Sequence[str],
146+
) -> Receiver[Mapping[str, Any]]: ...
147+
135148
@overload
136149
async def new_receiver( # pylint: disable=too-many-arguments
137150
self,
138151
*,
139152
wait_for_first: bool = True,
140153
skip_unchanged: bool = True,
154+
skip_none: Literal[False] = False,
141155
key: str | Sequence[str],
142156
schema: type[DataclassT],
143157
base_schema: type[Schema] | None,
144158
**marshmallow_load_kwargs: Any,
145159
) -> Receiver[DataclassT | None]: ...
146160

161+
@overload
162+
async def new_receiver( # pylint: disable=too-many-arguments
163+
self,
164+
*,
165+
wait_for_first: bool = True,
166+
skip_unchanged: bool = True,
167+
skip_none: Literal[True] = True,
168+
key: str | Sequence[str],
169+
schema: type[DataclassT],
170+
base_schema: type[Schema] | None,
171+
**marshmallow_load_kwargs: Any,
172+
) -> Receiver[DataclassT]: ...
173+
147174
# The noqa DOC502 is needed because we raise TimeoutError indirectly.
148-
async def new_receiver( # pylint: disable=too-many-arguments # noqa: DOC502
175+
# pylint: disable-next=too-many-arguments,too-many-locals
176+
async def new_receiver( # noqa: DOC502
149177
self,
150178
*,
151179
wait_for_first: bool = False,
152180
skip_unchanged: bool = True,
181+
skip_none: bool = True,
153182
# This is tricky, because a str is also a Sequence[str], if we would use only
154183
# Sequence[str], then a regular string would also be accepted and taken as
155184
# a sequence, like "key" -> ["k", "e", "y"]. We should never remove the str from
@@ -181,6 +210,13 @@ async def new_receiver( # pylint: disable=too-many-arguments # noqa: DOC502
181210
The comparison is done using the *raw* `dict` to determine if the configuration
182211
has changed.
183212
213+
If `skip_none` is set to `True`, then a configuration that is `None` will be
214+
ignored and not sent to the receiver. This is useful for cases where the the
215+
receiver can't react to `None` configurations, either because it is handled
216+
externally or because it should just keep the previous configuration.
217+
This can only be used when `key` is not `None` as when `key` is `None`, the
218+
configuration can never be `None`.
219+
184220
### Filtering
185221
186222
The configuration can be filtered by a `key`.
@@ -238,6 +274,8 @@ async def new_receiver( # pylint: disable=too-many-arguments # noqa: DOC502
238274
[`consume()`][frequenz.channels.Receiver.consume] on the receiver.
239275
skip_unchanged: Whether to skip sending the configuration if it hasn't
240276
changed compared to the last one received.
277+
skip_none: Whether to skip sending the configuration if it is `None`. Only
278+
valid when `key` is not `None`.
241279
key: The key to filter the configuration. If `None`, the full configuration
242280
will be received.
243281
schema: The type of the configuration. If provided, the configuration
@@ -322,12 +360,22 @@ def _is_valid_or_none(
322360
"""Return whether the configuration is valid or `None`."""
323361
return config is not _INVALID_CONFIG
324362

325-
def _is_valid(
326-
config: DataclassT | _InvalidConfig,
363+
def _is_valid_and_not_none(
364+
config: DataclassT | _InvalidConfig | None,
327365
) -> TypeGuard[DataclassT]:
328366
"""Return whether the configuration is valid and not `None`."""
329367
return config is not _INVALID_CONFIG
330368

369+
def _is_dataclass(config: DataclassT | None) -> TypeGuard[DataclassT]:
370+
"""Return whether the configuration is a dataclass."""
371+
return config is not None
372+
373+
def _is_mapping(
374+
config: Mapping[str, Any] | None
375+
) -> TypeGuard[Mapping[str, Any]]:
376+
"""Return whether the configuration is a mapping."""
377+
return config is not None
378+
331379
recv_name = f"{self}_receiver" if key is None else f"{self}_receiver_{key}"
332380
receiver = self.config_channel.new_receiver(name=recv_name, limit=1)
333381

@@ -355,12 +403,22 @@ def _is_valid(
355403
base_schema=base_schema,
356404
**marshmallow_load_kwargs,
357405
)
358-
).filter(_is_valid)
406+
).filter(_is_valid_and_not_none)
359407
assert_type(recv_dataclass, Receiver[DataclassT])
360408
return recv_dataclass
361409
case (str(), None):
362410
recv_map_or_none = receiver.map(lambda config: _get_key(config, key))
363411
assert_type(recv_map_or_none, Receiver[Mapping[str, Any] | None])
412+
if skip_none:
413+
# For some reason mypy is having trouble narrowing the type here,
414+
# so we need to cast it (pyright narrowes it correctly).
415+
recv_map = cast(
416+
Receiver[Mapping[str, Any]],
417+
recv_map_or_none.filter(_is_mapping),
418+
)
419+
assert_type(recv_map, Receiver[Mapping[str, Any]])
420+
return recv_map
421+
assert_type(recv_map_or_none, Receiver[Mapping[str, Any] | None])
364422
return recv_map_or_none
365423
case (str(), type()):
366424
recv_dataclass_or_none = receiver.map(
@@ -373,6 +431,10 @@ def _is_valid(
373431
)
374432
).filter(_is_valid_or_none)
375433
assert_type(recv_dataclass_or_none, Receiver[DataclassT | None])
434+
if skip_none:
435+
recv_dataclass = recv_dataclass_or_none.filter(_is_dataclass)
436+
assert_type(recv_dataclass, Receiver[DataclassT])
437+
return recv_dataclass
376438
return recv_dataclass_or_none
377439
case unexpected:
378440
# We can't use `assert_never` here because `mypy` is

0 commit comments

Comments
 (0)