|
4 | 4 | """Utilities to deal with configuration."""
|
5 | 5 |
|
6 | 6 | from collections.abc import Mapping
|
7 |
| -from typing import Any, TypeVar, cast |
| 7 | +from typing import Any, ClassVar, Protocol, TypeVar, cast |
8 | 8 |
|
| 9 | +from marshmallow import Schema |
9 | 10 | from marshmallow_dataclass import class_schema
|
10 | 11 |
|
11 |
| -T = TypeVar("T") |
| 12 | + |
| 13 | +# This is a hack that relies on identifying dataclasses by looking into an undocumented |
| 14 | +# property of dataclasses[1], so it might break in the future. Nevertheless, it seems to |
| 15 | +# be widely used in the community, for example `mypy` and `pyright` seem to rely on |
| 16 | +# it[2]. |
| 17 | +# |
| 18 | +# [1]: https://github.com/python/mypy/issues/15974#issuecomment-1694781006 |
| 19 | +# [2]: https://github.com/python/mypy/issues/15974#issuecomment-1694993493 |
| 20 | +class Dataclass(Protocol): |
| 21 | + """A protocol for dataclasses.""" |
| 22 | + |
| 23 | + __dataclass_fields__: ClassVar[dict[str, Any]] |
| 24 | + """The fields of the dataclass.""" |
| 25 | + |
| 26 | + |
| 27 | +DataclassT = TypeVar("DataclassT", bound=Dataclass) |
12 | 28 | """Type variable for configuration classes."""
|
13 | 29 |
|
14 | 30 |
|
15 | 31 | def load_config(
|
16 |
| - cls: type[T], |
| 32 | + cls: type[DataclassT], |
17 | 33 | config: Mapping[str, Any],
|
18 | 34 | /,
|
| 35 | + base_schema: type[Schema] | None = None, |
19 | 36 | **marshmallow_load_kwargs: Any,
|
20 |
| -) -> T: |
| 37 | +) -> DataclassT: |
21 | 38 | """Load a configuration from a dictionary into an instance of a configuration class.
|
22 | 39 |
|
23 | 40 | The configuration class is expected to be a [`dataclasses.dataclass`][], which is
|
24 | 41 | used to create a [`marshmallow.Schema`][] schema to validate the configuration
|
25 |
| - dictionary. |
| 42 | + dictionary using [`marshmallow_dataclass.class_schema`][] (which in turn uses the |
| 43 | + [`marshmallow.Schema.load`][] method to do the validation and deserialization). |
26 | 44 |
|
27 |
| - To customize the schema derived from the configuration dataclass, you can use |
28 |
| - [`marshmallow_dataclass.dataclass`][] to specify extra metadata. |
| 45 | + To customize the schema derived from the configuration dataclass, you can use the |
| 46 | + `metadata` key in [`dataclasses.field`][] to pass extra options to |
| 47 | + [`marshmallow_dataclass`][] to be used during validation and deserialization. |
29 | 48 |
|
30 | 49 | Additional arguments can be passed to [`marshmallow.Schema.load`][] using keyword
|
31 |
| - arguments. |
| 50 | + arguments `marshmallow_load_kwargs`. |
| 51 | +
|
| 52 | + Note: |
| 53 | + This method will raise [`marshmallow.ValidationError`][] if the configuration |
| 54 | + dictionary is invalid and you have to have in mind all of the gotchas of |
| 55 | + [`marshmallow`][] and [`marshmallow_dataclass`][] applies when using this |
| 56 | + function. It is recommended to carefully read the documentation of these |
| 57 | + libraries. |
32 | 58 |
|
33 | 59 | Args:
|
34 | 60 | cls: The configuration class.
|
35 | 61 | config: The configuration dictionary.
|
| 62 | + base_schema: An optional class to be used as a base schema for the configuration |
| 63 | + class. This allow using custom fields for example. Will be passed to |
| 64 | + [`marshmallow_dataclass.class_schema`][]. |
36 | 65 | **marshmallow_load_kwargs: Additional arguments to be passed to
|
37 | 66 | [`marshmallow.Schema.load`][].
|
38 | 67 |
|
39 | 68 | Returns:
|
40 | 69 | The loaded configuration as an instance of the configuration class.
|
41 | 70 | """
|
42 |
| - instance = class_schema(cls)().load(config, **marshmallow_load_kwargs) |
| 71 | + instance = class_schema(cls, base_schema)().load(config, **marshmallow_load_kwargs) |
43 | 72 | # We need to cast because `.load()` comes from marshmallow and doesn't know which
|
44 | 73 | # type is returned.
|
45 |
| - return cast(T, instance) |
| 74 | + return cast(DataclassT, instance) |
0 commit comments