Skip to content

Commit d087831

Browse files
committed
Add a function to load configurations with correct type hints
When loading configurations from dictionaries, using `marshmallow_dataclass`, the type hints are not preserved by the `load()` method, leading to repetitive casting every time `load()` needs to be used. This commit adds a new `load_config()` function to the `frequenz.sdk.config` module that takes care of loading configurations from dictionaries into configuration classes with correct type hints. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 6a8366e commit d087831

File tree

4 files changed

+77
-0
lines changed

4 files changed

+77
-0
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
## New Features
2222

2323
- The `ConfigManagingActor` can now take multiple configuration files as input, allowing to override default configurations with custom configurations.
24+
* A new `frequenz.sdk.config.load_config()` function is available to load configurations using `marshmallow_dataclass`es with correct type hints.
2425
- Implement and standardize logging configuration with the following changes:
2526
* Add `LoggerConfig` and `LoggingConfig` to standardize logging configuration.
2627
* Create `LoggingConfigUpdater` to handle runtime config updates.

src/frequenz/sdk/config/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55

66
from ._config_managing import ConfigManagingActor
77
from ._logging_config_updater import LoggerConfig, LoggingConfig, LoggingConfigUpdater
8+
from ._util import load_config
89

910
__all__ = [
1011
"ConfigManagingActor",
1112
"LoggingConfig",
1213
"LoggerConfig",
1314
"LoggingConfigUpdater",
15+
"load_config",
1416
]

src/frequenz/sdk/config/_util.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Utilities to deal with configuration."""
5+
6+
from collections.abc import Mapping
7+
from typing import Any, TypeVar, cast
8+
9+
from marshmallow_dataclass import class_schema
10+
11+
T = TypeVar("T")
12+
"""Type variable for configuration classes."""
13+
14+
15+
def load_config(
16+
cls: type[T],
17+
config: Mapping[str, Any],
18+
/,
19+
**marshmallow_load_kwargs: Any,
20+
) -> T:
21+
"""Load a configuration from a dictionary into an instance of a configuration class.
22+
23+
The configuration class is expected to be a [`marshmallow_dataclass.dataclass`][].
24+
Additional arguments can be passed to [`marshmallow.Schema.load`][] using keyword
25+
arguments.
26+
27+
Args:
28+
cls: The configuration class.
29+
config: The configuration dictionary.
30+
**marshmallow_load_kwargs: Additional arguments to be passed to
31+
[`marshmallow.Schema.load`][].
32+
33+
Returns:
34+
The loaded configuration as an instance of the configuration class.
35+
"""
36+
instance = class_schema(cls)().load(config, **marshmallow_load_kwargs)
37+
# We need to cast because `.load()` comes from marshmallow and doesn't know which
38+
# type is returned.
39+
return cast(T, instance)

tests/config/test_util.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for the config utilities."""
5+
6+
from typing import Any
7+
8+
import marshmallow_dataclass
9+
from pytest_mock import MockerFixture
10+
11+
from frequenz.sdk.config._util import load_config
12+
13+
14+
@marshmallow_dataclass.dataclass
15+
class SimpleConfig:
16+
"""A simple configuration class for testing."""
17+
18+
name: str
19+
value: int
20+
21+
22+
def test_load_config(mocker: MockerFixture) -> None:
23+
"""Test that load_config loads a configuration into a configuration class."""
24+
mock_class_schema = mocker.Mock()
25+
mock_class_schema.return_value.load.return_value = {"name": "test", "value": 42}
26+
mocker.patch(
27+
"frequenz.sdk.config._util.class_schema", return_value=mock_class_schema
28+
)
29+
config: dict[str, Any] = {}
30+
31+
# We add the type hint to test that the return type (hint) is correct
32+
_: SimpleConfig = load_config(SimpleConfig, config, marshmallow_arg=1)
33+
mock_class_schema.return_value.load.assert_called_once_with(
34+
config, marshmallow_arg=1
35+
)

0 commit comments

Comments
 (0)