Skip to content

Commit 6fd906b

Browse files
authored
Move default unknown=EXCLUDE to BaseConfigSchema (#1149)
This simplifies the code as we don't need to manipulate the `load()` arguments in the wrapper functions. It also uses `BaseConfigSchema` as the default base schema in `load_config()`.
2 parents 6902fd7 + b03ffd7 commit 6fd906b

File tree

7 files changed

+67
-48
lines changed

7 files changed

+67
-48
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ 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.
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.
35+
+ Will now raise a `ValueError` if `unknown` is set to `INCLUDE` in `marshmallow_load_kwargs`.
3536

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

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: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
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

13+
from ._base_schema import BaseConfigSchema
14+
1215

1316
# This is a hack that relies on identifying dataclasses by looking into an undocumented
1417
# property of dataclasses[1], so it might break in the future. Nevertheless, it seems to
@@ -33,7 +36,7 @@ def load_config(
3336
config: Mapping[str, Any],
3437
/,
3538
*,
36-
base_schema: type[Schema] | None = None,
39+
base_schema: type[Schema] | None = BaseConfigSchema,
3740
marshmallow_load_kwargs: dict[str, Any] | None = None,
3841
) -> DataclassT:
3942
"""Load a configuration from a dictionary into an instance of a configuration class.
@@ -69,9 +72,32 @@ def load_config(
6972
Returns:
7073
The loaded configuration as an instance of the configuration class.
7174
"""
75+
_validate_load_kwargs(marshmallow_load_kwargs)
76+
7277
instance = class_schema(cls, base_schema)().load(
7378
config, **(marshmallow_load_kwargs or {})
7479
)
7580
# We need to cast because `.load()` comes from marshmallow and doesn't know which
7681
# type is returned.
7782
return cast(DataclassT, instance)
83+
84+
85+
def _validate_load_kwargs(marshmallow_load_kwargs: dict[str, Any] | None) -> None:
86+
"""Validate the marshmallow load kwargs.
87+
88+
This function validates the `unknown` option of the marshmallow load kwargs to
89+
prevent loading unknown fields when loading to a dataclass.
90+
91+
Args:
92+
marshmallow_load_kwargs: The dictionary to get the marshmallow load kwargs from.
93+
94+
Raises:
95+
ValueError: If the `unknown` option is set to [`marshmallow.INCLUDE`][].
96+
"""
97+
if (
98+
marshmallow_load_kwargs
99+
and marshmallow_load_kwargs.get("unknown") == marshmallow.INCLUDE
100+
):
101+
raise ValueError(
102+
"The 'unknown' option can't be 'INCLUDE' when loading to a dataclass"
103+
)

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)