Skip to content

Commit 61d4b11

Browse files
committed
Remove support for receiving raw mapping as configuration
This seems to be a very niche feature that adds quite a bit of complexity. Users than need this kind of raw access can just get a receiver from the `config_channel` themselves and do the processing they need. Now the `schema` is required, was renamed to `config_class` for extra clarity and is a positional-only argument. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 1f96cec commit 61d4b11

File tree

1 file changed

+44
-91
lines changed

1 file changed

+44
-91
lines changed

src/frequenz/sdk/config/_manager.py

Lines changed: 44 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import pathlib
88
from collections.abc import Mapping, Sequence
99
from 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
1213
from frequenz.channels import Broadcast, Receiver
1314
from frequenz.channels.experimental import WithPrevious
1415
from 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

Comments
 (0)