Skip to content

Commit 5f3f5d2

Browse files
committed
Move default unknown=EXCLUDE to BaseConfigSchema
This simplifies the code as we don't need to manipulate the `load()` arguments in the wrapper functions and also brings the default to `load_config()`. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 1962c25 commit 5f3f5d2

File tree

7 files changed

+61
-47
lines changed

7 files changed

+61
-47
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ This release includes a new `ConfigManager` class to simplify managing the confi
3030

3131
* `load_config()`:
3232

33-
+ The `base_schema` argument is now keyword-only and defaults to `BaseConfigSchema`.
33+
+ The `base_schema` argument is now keyword-only and defaults to `BaseConfigSchema` (and because of this, it uses `unknown=EXCLUDE` by default).
3434
+ The arguments forwarded to `marshmallow.Schema.load()` now must be passed explicitly via the `marshmallow_load_kwargs` argument, as a `dict`, to improve the type-checking.
3535

3636
* `ConfigManagingActor`: Raise a `ValueError` if the `config_files` argument an empty sequence.

src/frequenz/sdk/config/__init__.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ class AppConfig:
9393
9494
Customization can also be done via a `base_schema`. By default
9595
[`BaseConfigSchema`][frequenz.sdk.config.BaseConfigSchema] is used to provide support
96-
for some extra commonly used fields (like [quantities][frequenz.quantities]).
96+
for some extra commonly used fields (like [quantities][frequenz.quantities]) and to
97+
exclude unknown fields by default.
9798
9899
```python
99100
import marshmallow.validate
@@ -109,11 +110,7 @@ class Config:
109110
Additional arguments can be passed to [`marshmallow.Schema.load`][] using
110111
the `marshmallow_load_kwargs` keyword arguments.
111112
112-
If unspecified, the `marshmallow_load_kwargs` will have the `unknown` key set to
113-
[`marshmallow.EXCLUDE`][] (instead of the normal [`marshmallow.RAISE`][]
114-
default).
115-
116-
But when [`marshmallow.EXCLUDE`][] is used, a warning will be logged if there are extra
113+
When [`marshmallow.EXCLUDE`][] is used, a warning will be logged if there are extra
117114
fields in the configuration that are excluded. This is useful, for example, to catch
118115
typos in the configuration file.
119116

src/frequenz/sdk/config/_base_schema.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,18 @@
33

44
"""Base schema for configuration classes."""
55

6+
import marshmallow
67
from frequenz.quantities.experimental.marshmallow import QuantitySchema
78

89

910
class BaseConfigSchema(QuantitySchema):
10-
"""A base schema for configuration classes."""
11+
"""A base schema for configuration classes.
12+
13+
This schema provides validation for quantities and ignores unknown fields by
14+
default.
15+
"""
16+
17+
class Meta:
18+
"""Meta options for the schema."""
19+
20+
unknown = marshmallow.EXCLUDE

src/frequenz/sdk/config/_manager.py

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from ..actor._background_service import BackgroundService
2121
from ._base_schema import BaseConfigSchema
2222
from ._managing_actor import ConfigManagingActor
23-
from ._util import DataclassT, load_config
23+
from ._util import DataclassT, _validate_load_kwargs, load_config
2424

2525
_logger = logging.getLogger(__name__)
2626

@@ -224,21 +224,8 @@ def new_receiver( # pylint: disable=too-many-arguments
224224
225225
Returns:
226226
The receiver for the configuration.
227-
228-
Raises:
229-
ValueError: If the `unknown` option in `marshmallow_load_kwargs` is set to
230-
[`marshmallow.INCLUDE`][].
231227
"""
232-
marshmallow_load_kwargs = (
233-
{} if marshmallow_load_kwargs is None else marshmallow_load_kwargs.copy()
234-
)
235-
236-
if "unknown" not in marshmallow_load_kwargs:
237-
marshmallow_load_kwargs["unknown"] = marshmallow.EXCLUDE
238-
elif marshmallow_load_kwargs["unknown"] == marshmallow.INCLUDE:
239-
raise ValueError(
240-
"The 'unknown' option can't be 'INCLUDE' when loading to a dataclass"
241-
)
228+
_validate_load_kwargs(marshmallow_load_kwargs)
242229

243230
receiver = self.config_channel.new_receiver(name=f"{self}:{key}", limit=1).map(
244231
lambda config: _load_config_with_logging_and_errors(
@@ -493,23 +480,22 @@ def _load_config(
493480
{} if marshmallow_load_kwargs is None else marshmallow_load_kwargs.copy()
494481
)
495482

496-
unknown = marshmallow_load_kwargs.get("unknown")
497-
if unknown == marshmallow.EXCLUDE:
498-
# When excluding unknown fields we still want to notify the user, as
499-
# this could mean there is a typo in the configuration and some value is
500-
# not being loaded as desired.
501-
marshmallow_load_kwargs["unknown"] = marshmallow.RAISE
502-
try:
503-
load_config(
504-
config_class,
505-
config,
506-
base_schema=base_schema,
507-
marshmallow_load_kwargs=marshmallow_load_kwargs,
508-
)
509-
except ValidationError as err:
510-
_logger.warning(
511-
"The configuration for key %r has extra fields that will be ignored: %s",
512-
key,
513-
err,
514-
)
483+
# When excluding unknown fields we still want to notify the user, as
484+
# this could mean there is a typo in the configuration and some value is
485+
# not being loaded as desired.
486+
marshmallow_load_kwargs["unknown"] = marshmallow.RAISE
487+
try:
488+
load_config(
489+
config_class,
490+
config,
491+
base_schema=base_schema,
492+
marshmallow_load_kwargs=marshmallow_load_kwargs,
493+
)
494+
except ValidationError as err:
495+
_logger.warning(
496+
"The configuration for key %r has extra fields that will be ignored: %s",
497+
key,
498+
err,
499+
)
500+
515501
return loaded_config

src/frequenz/sdk/config/_util.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from collections.abc import Mapping
77
from typing import Any, ClassVar, Protocol, TypeVar, cast
88

9+
import marshmallow
910
from marshmallow import Schema
1011
from marshmallow_dataclass import class_schema
1112

@@ -77,3 +78,24 @@ def load_config(
7778
# We need to cast because `.load()` comes from marshmallow and doesn't know which
7879
# type is returned.
7980
return cast(DataclassT, instance)
81+
82+
83+
def _validate_load_kwargs(marshmallow_load_kwargs: dict[str, Any] | None) -> None:
84+
"""Validate the marshmallow load kwargs.
85+
86+
This function validates the `unknown` option of the marshmallow load kwargs to
87+
prevent loading unknown fields when loading to a dataclass.
88+
89+
Args:
90+
marshmallow_load_kwargs: The dictionary to get the marshmallow load kwargs from.
91+
92+
Raises:
93+
ValueError: If the `unknown` option is set to [`marshmallow.INCLUDE`][].
94+
"""
95+
if (
96+
marshmallow_load_kwargs
97+
and marshmallow_load_kwargs.get("unknown") == marshmallow.INCLUDE
98+
):
99+
raise ValueError(
100+
"The 'unknown' option can't be 'INCLUDE' when loading to a dataclass"
101+
)

tests/config/test_logging_actor.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ def test_logging_config() -> None:
4848
load_config(LoggingConfig, config_raw)
4949

5050
config_raw = {"unknown": {"frequenz.sdk.actor": {"level": "DEBUG"}}}
51-
with pytest.raises(ValidationError):
52-
load_config(LoggingConfig, config_raw)
51+
assert load_config(LoggingConfig, config_raw) == config
5352

5453

5554
@pytest.fixture

tests/config/test_util.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,15 @@ class _MyBaseSchema(marshmallow.Schema):
7979
class Meta:
8080
"""Meta options for the schema."""
8181

82-
unknown = marshmallow.EXCLUDE
82+
unknown = marshmallow.RAISE
8383

8484
config: dict[str, Any] = {"name": "test", "value": 42, "extra": "extra"}
8585

86-
loaded_config = load_config(config_class, config, base_schema=_MyBaseSchema)
86+
loaded_config = load_config(config_class, config)
8787
assert loaded_config == config_class(name="test", value=42)
8888

8989
with pytest.raises(marshmallow.ValidationError):
90-
_ = load_config(config_class, config)
90+
_ = load_config(config_class, config, base_schema=_MyBaseSchema)
9191

9292

9393
def test_load_config_type_hints(mocker: MockerFixture) -> None:

0 commit comments

Comments
 (0)