|
9 | 9 | from datetime import timedelta |
10 | 10 | from typing import Any, Final |
11 | 11 |
|
| 12 | +import marshmallow |
12 | 13 | from frequenz.channels import Broadcast, Receiver |
13 | 14 | from frequenz.channels.experimental import WithPrevious |
14 | 15 | from marshmallow import Schema, ValidationError |
@@ -177,6 +178,14 @@ def new_receiver( # pylint: disable=too-many-arguments |
177 | 178 | Additional arguments can be passed to [`marshmallow.Schema.load`][] using |
178 | 179 | the `marshmallow_load_kwargs` keyword arguments. |
179 | 180 |
|
| 181 | + If unspecified, the `marshmallow_load_kwargs` will have the `unknown` key set to |
| 182 | + [`marshmallow.EXCLUDE`][] (instead of the normal [`marshmallow.RAISE`][] |
| 183 | + default). |
| 184 | +
|
| 185 | + But when [`marshmallow.EXCLUDE`][] is used, a warning will still be logged if |
| 186 | + there are extra fields in the configuration that are excluded. This is useful, |
| 187 | + for example, to catch typos in the configuration file. |
| 188 | +
|
180 | 189 | ### Skipping superfluous updates |
181 | 190 |
|
182 | 191 | If there is a burst of configuration updates, the receiver will only receive the |
@@ -220,7 +229,22 @@ def new_receiver( # pylint: disable=too-many-arguments |
220 | 229 |
|
221 | 230 | Returns: |
222 | 231 | The receiver for the configuration. |
| 232 | +
|
| 233 | + Raises: |
| 234 | + ValueError: If the `unknown` option in `marshmallow_load_kwargs` is set to |
| 235 | + [`marshmallow.INCLUDE`][]. |
223 | 236 | """ |
| 237 | + marshmallow_load_kwargs = ( |
| 238 | + {} if marshmallow_load_kwargs is None else marshmallow_load_kwargs.copy() |
| 239 | + ) |
| 240 | + |
| 241 | + if "unknown" not in marshmallow_load_kwargs: |
| 242 | + marshmallow_load_kwargs["unknown"] = marshmallow.EXCLUDE |
| 243 | + elif marshmallow_load_kwargs["unknown"] == marshmallow.INCLUDE: |
| 244 | + raise ValueError( |
| 245 | + "The 'unknown' option can't be 'INCLUDE' when loading to a dataclass" |
| 246 | + ) |
| 247 | + |
224 | 248 | receiver = self.config_channel.new_receiver(name=f"{self}:{key}", limit=1).map( |
225 | 249 | lambda config: _load_config_with_logging_and_errors( |
226 | 250 | config, |
@@ -286,9 +310,10 @@ def _load_config_with_logging_and_errors( |
286 | 310 | _logger.debug("Configuration key %r not found, sending None", key) |
287 | 311 | return None |
288 | 312 |
|
289 | | - loaded_config = load_config( |
290 | | - config_class, |
| 313 | + loaded_config = _load_config( |
291 | 314 | sub_config, |
| 315 | + config_class, |
| 316 | + key=key, |
292 | 317 | base_schema=base_schema, |
293 | 318 | marshmallow_load_kwargs=marshmallow_load_kwargs, |
294 | 319 | ) |
@@ -356,3 +381,45 @@ def _get_key( |
356 | 381 | ) |
357 | 382 | value = new_value |
358 | 383 | return value |
| 384 | + |
| 385 | + |
| 386 | +def _load_config( |
| 387 | + config: Mapping[str, Any], |
| 388 | + config_class: type[DataclassT], |
| 389 | + *, |
| 390 | + key: str | Sequence[str], |
| 391 | + base_schema: type[Schema] | None = None, |
| 392 | + marshmallow_load_kwargs: dict[str, Any] | None = None, |
| 393 | +) -> DataclassT | InvalidValueForKeyError | ValidationError | None: |
| 394 | + """Try to load a configuration and log any validation errors.""" |
| 395 | + loaded_config = load_config( |
| 396 | + config_class, |
| 397 | + config, |
| 398 | + base_schema=base_schema, |
| 399 | + marshmallow_load_kwargs=marshmallow_load_kwargs, |
| 400 | + ) |
| 401 | + |
| 402 | + marshmallow_load_kwargs = ( |
| 403 | + {} if marshmallow_load_kwargs is None else marshmallow_load_kwargs.copy() |
| 404 | + ) |
| 405 | + |
| 406 | + unknown = marshmallow_load_kwargs.get("unknown") |
| 407 | + if unknown == marshmallow.EXCLUDE: |
| 408 | + # When excluding unknown fields we still want to notify the user, as |
| 409 | + # this could mean there is a typo in the configuration and some value is |
| 410 | + # not being loaded as desired. |
| 411 | + marshmallow_load_kwargs["unknown"] = marshmallow.RAISE |
| 412 | + try: |
| 413 | + load_config( |
| 414 | + config_class, |
| 415 | + config, |
| 416 | + base_schema=base_schema, |
| 417 | + marshmallow_load_kwargs=marshmallow_load_kwargs, |
| 418 | + ) |
| 419 | + except ValidationError as err: |
| 420 | + _logger.warning( |
| 421 | + "The configuration for key %r has extra fields that will be ignored: %s", |
| 422 | + key, |
| 423 | + err, |
| 424 | + ) |
| 425 | + return loaded_config |
0 commit comments