88import pathlib
99from collections .abc import Mapping , Sequence
1010from 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
1313from frequenz .channels import Broadcast , Receiver
1414from 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