77import pathlib
88from collections .abc import Mapping , Sequence
99from datetime import timedelta
10- from typing import Any , Final , Literal , TypeGuard , assert_type , cast , overload
10+ from typing import Any , Final , Literal , TypeGuard , assert_type , overload
1111
12+ import marshmallow
1213from frequenz .channels import Broadcast , Receiver
1314from frequenz .channels .experimental import WithPrevious
1415from marshmallow import Schema , ValidationError
@@ -100,67 +101,44 @@ def __str__(self) -> str:
100101 """Return a string representation of this config manager."""
101102 return f"{ type (self ).__name__ } [{ self .name } ]"
102103
103- @overload
104- def new_receiver (
105- self ,
106- * ,
107- skip_unchanged : bool = True ,
108- skip_none : Literal [False ] = False ,
109- ) -> Receiver [Mapping [str , Any ]]: ...
110-
111104 @overload
112105 def new_receiver ( # pylint: disable=too-many-arguments
113106 self ,
107+ config_class : type [DataclassT ],
108+ / ,
114109 * ,
115110 skip_unchanged : bool = True ,
116111 skip_none : Literal [False ] = False ,
117112 # We need to specify the key here because we have kwargs, so if it is not
118113 # present is not considered None as the only possible value, as any value can be
119114 # accepted as part of the kwargs.
120115 key : None = None ,
121- schema : type [DataclassT ],
122116 base_schema : type [Schema ] | None = None ,
123117 ** marshmallow_load_kwargs : Any ,
124118 ) -> Receiver [DataclassT ]: ...
125119
126- @overload
127- def new_receiver (
128- self ,
129- * ,
130- skip_unchanged : bool = True ,
131- skip_none : Literal [False ] = False ,
132- key : str | Sequence [str ],
133- ) -> Receiver [Mapping [str , Any ] | None ]: ...
134-
135- @overload
136- def new_receiver (
137- self ,
138- * ,
139- skip_unchanged : bool = True ,
140- skip_none : Literal [True ] = True ,
141- key : str | Sequence [str ],
142- ) -> Receiver [Mapping [str , Any ]]: ...
143-
144120 @overload
145121 def new_receiver ( # pylint: disable=too-many-arguments
146122 self ,
123+ config_class : type [DataclassT ],
124+ / ,
147125 * ,
148126 skip_unchanged : bool = True ,
149127 skip_none : Literal [False ] = False ,
150128 key : str | Sequence [str ],
151- schema : type [DataclassT ],
152129 base_schema : type [Schema ] | None ,
153130 ** marshmallow_load_kwargs : Any ,
154131 ) -> Receiver [DataclassT | None ]: ...
155132
156133 @overload
157134 def new_receiver ( # pylint: disable=too-many-arguments
158135 self ,
136+ config_class : type [DataclassT ],
137+ / ,
159138 * ,
160139 skip_unchanged : bool = True ,
161140 skip_none : Literal [True ] = True ,
162141 key : str | Sequence [str ],
163- schema : type [DataclassT ],
164142 base_schema : type [Schema ] | None ,
165143 ** marshmallow_load_kwargs : Any ,
166144 ) -> Receiver [DataclassT ]: ...
@@ -169,6 +147,8 @@ def new_receiver( # pylint: disable=too-many-arguments
169147 # pylint: disable-next=too-many-arguments,too-many-locals
170148 def new_receiver ( # noqa: DOC502
171149 self ,
150+ config_class : type [DataclassT ],
151+ / ,
172152 * ,
173153 skip_unchanged : bool = True ,
174154 skip_none : bool = True ,
@@ -178,15 +158,9 @@ def new_receiver( # noqa: DOC502
178158 # the allowed types without changing Sequence[str] to something more specific,
179159 # like list[str] or tuple[str].
180160 key : str | Sequence [str ] | None = None ,
181- schema : type [DataclassT ] | None = None ,
182161 base_schema : type [Schema ] | None = None ,
183162 ** marshmallow_load_kwargs : Any ,
184- ) -> (
185- Receiver [Mapping [str , Any ]]
186- | Receiver [Mapping [str , Any ] | None ]
187- | Receiver [DataclassT ]
188- | Receiver [DataclassT | None ]
189- ):
163+ ) -> Receiver [DataclassT ] | Receiver [DataclassT | None ]:
190164 """Create a new receiver for the configuration.
191165
192166 This method has a lot of features and functionalities to make it easier to
@@ -196,6 +170,25 @@ def new_receiver( # noqa: DOC502
196170 If there is a burst of configuration updates, the receiver will only
197171 receive the last configuration, older configurations will be ignored.
198172
173+ ### Schema validation
174+
175+ The raw configuration received as a `Mapping` will be validated and loaded to
176+ as a `config_class`. The `config_class` class is expected to be
177+ a [`dataclasses.dataclass`][], which is used to create
178+ a [`marshmallow.Schema`][] via the [`marshmallow_dataclass.class_schema`][]
179+ function.
180+
181+ This means you can customize the schema derived from the configuration
182+ dataclass using [`marshmallow_dataclass`][] to specify extra validation and
183+ options via field metadata.
184+
185+ Configurations that don't pass the validation will be ignored and not sent to
186+ the receiver, but an error will be logged. Errors other than `ValidationError`
187+ will not be handled and will be raised.
188+
189+ Additional arguments can be passed to [`marshmallow.Schema.load`][] using
190+ the `marshmallow_load_kwargs` keyword arguments.
191+
199192 ### Skipping superfluous updates
200193
201194 If `skip_unchanged` is set to `True`, then a configuration that didn't change
@@ -222,40 +215,20 @@ def new_receiver( # noqa: DOC502
222215 receiver will receive the configuration under the nested key. For example
223216 `["key", "subkey"]` will get only `config["key"]["subkey"]`.
224217
225- ### Schema validation
226-
227- The configuration is received as a dictionary unless a `schema` is provided. In
228- this case, the configuration will be validated against the schema and received
229- as an instance of the configuration class.
230-
231- The configuration `schema` class is expected to be
232- a [`dataclasses.dataclass`][], which is used to create
233- a [`marshmallow.Schema`][] schema to validate the configuration dictionary.
234-
235- To customize the schema derived from the configuration dataclass, you can
236- use [`marshmallow_dataclass.dataclass`][] to specify extra metadata.
237-
238- Configurations that don't pass the validation will be ignored and not sent to
239- the receiver, but an error will be logged. Errors other than `ValidationError`
240- will not be handled and will be raised.
241-
242- Additional arguments can be passed to [`marshmallow.Schema.load`][] using keyword
243- arguments.
244-
245218 Example:
246219 ```python
247220 # TODO: Add Example
248221 ```
249222
250223 Args:
224+ config_class: The type of the configuration. If provided, the configuration
225+ will be validated against this type.
251226 skip_unchanged: Whether to skip sending the configuration if it hasn't
252227 changed compared to the last one received.
253228 skip_none: Whether to skip sending the configuration if it is `None`. Only
254229 valid when `key` is not `None`.
255230 key: The key to filter the configuration. If `None`, the full configuration
256231 will be received.
257- schema: The type of the configuration. If provided, the configuration
258- will be validated against this type.
259232 base_schema: An optional class to be used as a base schema for the
260233 configuration class. This allow using custom fields for example. Will be
261234 passed to [`marshmallow_dataclass.class_schema`][].
@@ -274,7 +247,7 @@ def new_receiver( # noqa: DOC502
274247 @overload
275248 def _load_config_with_logging (
276249 config : Mapping [str , Any ],
277- schema : type [DataclassT ],
250+ config_class : type [DataclassT ],
278251 * ,
279252 key : None = None ,
280253 base_schema : type [Schema ] | None = None ,
@@ -284,7 +257,7 @@ def _load_config_with_logging(
284257 @overload
285258 def _load_config_with_logging (
286259 config : Mapping [str , Any ],
287- schema : type [DataclassT ],
260+ config_class : type [DataclassT ],
288261 * ,
289262 key : str | Sequence [str ],
290263 base_schema : type [Schema ] | None = None ,
@@ -293,7 +266,7 @@ def _load_config_with_logging(
293266
294267 def _load_config_with_logging (
295268 config : Mapping [str , Any ],
296- schema : type [DataclassT ],
269+ config_class : type [DataclassT ],
297270 * ,
298271 key : str | Sequence [str ] | None = None ,
299272 base_schema : type [Schema ] | None = None ,
@@ -313,7 +286,10 @@ def _load_config_with_logging(
313286
314287 try :
315288 return load_config (
316- schema , config , base_schema = base_schema , ** marshmallow_load_kwargs
289+ config_class ,
290+ config ,
291+ base_schema = base_schema ,
292+ ** marshmallow_load_kwargs ,
317293 )
318294 except ValidationError as err :
319295 key_str = ""
@@ -342,27 +318,18 @@ def _is_dataclass(config: DataclassT | None) -> TypeGuard[DataclassT]:
342318 """Return whether the configuration is a dataclass."""
343319 return config is not None
344320
345- def _is_mapping (
346- config : Mapping [str , Any ] | None
347- ) -> TypeGuard [Mapping [str , Any ]]:
348- """Return whether the configuration is a mapping."""
349- return config is not None
350-
351321 recv_name = f"{ self } _receiver" if key is None else f"{ self } _receiver_{ key } "
352322 receiver = self .config_channel .new_receiver (name = recv_name , limit = 1 )
353323
354324 if skip_unchanged :
355325 receiver = receiver .filter (WithPrevious (_NotEqualWithLogging (key )))
356326
357- match (key , schema ):
358- case (None , None ):
359- assert_type (receiver , Receiver [Mapping [str , Any ]])
360- return receiver
361- case (None , type ()):
327+ match key :
328+ case None :
362329 recv_dataclass = receiver .map (
363330 lambda config : _load_config_with_logging (
364331 config ,
365- schema ,
332+ config_class ,
366333 # we need to pass it explicitly because of the
367334 # variadic keyword arguments, otherwise key
368335 # could be included in marshmallow_load_kwargs
@@ -374,25 +341,11 @@ def _is_mapping(
374341 ).filter (_is_valid_and_not_none )
375342 assert_type (recv_dataclass , Receiver [DataclassT ])
376343 return recv_dataclass
377- case (str (), None ):
378- recv_map_or_none = receiver .map (lambda config : _get_key (config , key ))
379- assert_type (recv_map_or_none , Receiver [Mapping [str , Any ] | None ])
380- if skip_none :
381- # For some reason mypy is having trouble narrowing the type here,
382- # so we need to cast it (pyright narrowes it correctly).
383- recv_map = cast (
384- Receiver [Mapping [str , Any ]],
385- recv_map_or_none .filter (_is_mapping ),
386- )
387- assert_type (recv_map , Receiver [Mapping [str , Any ]])
388- return recv_map
389- assert_type (recv_map_or_none , Receiver [Mapping [str , Any ] | None ])
390- return recv_map_or_none
391- case (str (), type ()):
344+ case str ():
392345 recv_dataclass_or_none = receiver .map (
393346 lambda config : _load_config_with_logging (
394347 config ,
395- schema ,
348+ config_class ,
396349 key = key ,
397350 base_schema = base_schema ,
398351 ** marshmallow_load_kwargs ,
0 commit comments