Skip to content

Commit dcab630

Browse files
committed
Final round of fixes for multi value serializing mess
1 parent ab3911c commit dcab630

File tree

2 files changed

+37
-66
lines changed

2 files changed

+37
-66
lines changed

music_assistant_models/config_entries.py

Lines changed: 36 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections.abc import Callable, Iterable
77
from dataclasses import dataclass, field
88
from enum import Enum
9-
from typing import Any, cast
9+
from typing import Any, Final, cast
1010

1111
from mashumaro import DataClassDictMixin
1212

@@ -21,22 +21,16 @@
2121
ConfigValueType = (
2222
# order is important here for the (de)serialization!
2323
# https://github.com/Fatal1ty/mashumaro/pull/256
24-
bool | float | int | str | tuple[int, int]
25-
)
26-
ConfigValueTypeMulti = (
27-
# order is important here for the (de)serialization!
28-
# https://github.com/Fatal1ty/mashumaro/pull/256
29-
list[bool] | list[float] | list[int] | list[str] | list[tuple[int, int]]
24+
bool | float | int | str | list[float] | list[int] | list[str] | list[bool] | None
3025
)
3126

32-
ConfigValueTypes = ConfigValueType | ConfigValueTypeMulti | None
3327

3428
ConfigEntryTypeMap: dict[ConfigEntryType, type[ConfigValueType]] = {
3529
ConfigEntryType.BOOLEAN: bool,
3630
ConfigEntryType.STRING: str,
3731
ConfigEntryType.SECURE_STRING: str,
3832
ConfigEntryType.INTEGER: int,
39-
ConfigEntryType.INTEGER_TUPLE: tuple[int, int],
33+
ConfigEntryType.SPLITTED_STRING: str,
4034
ConfigEntryType.FLOAT: float,
4135
ConfigEntryType.LABEL: str,
4236
ConfigEntryType.DIVIDER: str,
@@ -61,6 +55,9 @@ class ConfigValueOption(DataClassDictMixin):
6155
value: ConfigValueType
6256

6357

58+
MULTI_VALUE_SPLITTER: Final[str] = "||"
59+
60+
6461
@dataclass(kw_only=True)
6562
class ConfigEntry(DataClassDictMixin):
6663
"""Model for a Config Entry.
@@ -75,7 +72,7 @@ class ConfigEntry(DataClassDictMixin):
7572
type: ConfigEntryType
7673
# label: default label when no translation for the key is present
7774
label: str
78-
default_value: ConfigValueType | ConfigValueTypeMulti | None = None
75+
default_value: ConfigValueType = None
7976
required: bool = True
8077
# options [optional]: select from list of possible values/options
8178
options: list[ConfigValueOption] = field(default_factory=list)
@@ -102,81 +99,58 @@ class ConfigEntry(DataClassDictMixin):
10299
# action_label: default label for the action when no translation for the action is present
103100
action_label: str | None = None
104101
# value: set by the config manager/flow (or in rare cases by the provider itself)
105-
value: ConfigValueType | ConfigValueTypeMulti | None = None
102+
value: ConfigValueType = None
106103

107104
def __post_init__(self) -> None:
108105
"""Run some basic sanity checks after init."""
109-
if self.multi_value and not isinstance(self, MultiValueConfigEntry):
110-
raise ValueError(f"{self.key} must be a MultiValueConfigEntry")
111106
if self.type in UI_ONLY:
112107
self.required = False
113108

114109
def parse_value(
115110
self,
116-
value: ConfigValueTypes,
111+
value: ConfigValueType,
117112
allow_none: bool = True,
118-
) -> ConfigValueTypes:
113+
) -> ConfigValueType:
119114
"""Parse value from the config entry details and plain value."""
120115
if self.type == ConfigEntryType.LABEL:
121116
value = self.label
122117
elif self.type in UI_ONLY:
123-
value = cast(str | None, value or self.default_value)
118+
value = value or self.default_value
124119

125-
if value is None and (not self.required or allow_none):
126-
value = cast(ConfigValueType | None, self.default_value)
120+
if value is None:
121+
value = self.default_value
127122

128123
if isinstance(value, list) and not self.multi_value:
129124
raise ValueError(f"{self.key} must be a single value")
125+
if self.multi_value and not isinstance(value, list):
126+
raise ValueError(f"value for {self.key} must be a list")
130127

131-
if value is None and self.required:
132-
raise ValueError(f"{self.key} is required")
133-
134-
self.value = value
135-
return self.value
136-
137-
138-
@dataclass(kw_only=True)
139-
class MultiValueConfigEntry(ConfigEntry):
140-
"""Model for a Config Entry which allows multiple values to be selected.
141-
142-
This is a helper class to handle multiple values in a single config entry,
143-
otherwise the serializer gets confused with the types.
144-
"""
145-
146-
multi_value: bool = True
147-
default_value: ConfigValueTypeMulti = field(default_factory=list)
148-
value: ConfigValueTypeMulti | None = None
149-
150-
def parse_value( # type: ignore[override]
151-
self,
152-
value: ConfigValueTypeMulti | None,
153-
allow_none: bool = True,
154-
) -> ConfigValueTypeMulti:
155-
"""Parse value from the config entry details and plain value."""
156-
if value is None and (not self.required or allow_none):
157-
value = self.default_value
158-
if value is None:
128+
if value is None and self.required and not allow_none:
159129
raise ValueError(f"{self.key} is required")
160-
if self.multi_value and not isinstance(value, list):
161-
raise ValueError(f"{self.key} must be a list")
162130

163131
self.value = value
164132
return self.value
165133

166-
def __post_init__(self) -> None:
167-
"""Run some basic sanity checks after init."""
168-
super().__post_init__()
169-
if self.multi_value and not isinstance(self.default_value, list):
170-
raise ValueError(f"default value for {self.key} must be a list")
134+
def get_splitted_values(self) -> tuple[str, ...] | list[tuple[str, ...]]:
135+
"""Return split values for SPLITTED_STRING type."""
136+
if self.type != ConfigEntryType.SPLITTED_STRING:
137+
raise ValueError(f"{self.key} is not a SPLITTED_STRING")
138+
value = self.value or self.default_value
139+
if self.multi_value:
140+
assert isinstance(value, list)
141+
value = cast(list[str], value)
142+
return [tuple(x.split(MULTI_VALUE_SPLITTER, 1)) for x in value]
143+
assert isinstance(value, str)
144+
return tuple(value.split(MULTI_VALUE_SPLITTER, 1))
171145

172146

173147
@dataclass
174148
class Config(DataClassDictMixin):
175149
"""Base Configuration object."""
176150

177-
values: dict[str, ConfigEntry | MultiValueConfigEntry]
151+
values: dict[str, ConfigEntry]
178152

179-
def get_value(self, key: str) -> ConfigValueTypes:
153+
def get_value(self, key: str) -> ConfigValueType:
180154
"""Return config value for given key."""
181155
config_value = self.values[key]
182156
if config_value.type == ConfigEntryType.SECURE_STRING and config_value.value:
@@ -189,7 +163,7 @@ def get_value(self, key: str) -> ConfigValueTypes:
189163
@classmethod
190164
def parse(
191165
cls,
192-
config_entries: Iterable[ConfigEntry | MultiValueConfigEntry],
166+
config_entries: Iterable[ConfigEntry],
193167
raw: dict[str, Any],
194168
) -> Config:
195169
"""Parse Config from the raw values (as stored in persistent storage)."""
@@ -199,10 +173,7 @@ def parse(
199173
if isinstance(entry.default_value, Enum):
200174
entry.default_value = entry.default_value.value # type: ignore[unreachable]
201175
# create a copy of the entry
202-
if entry.multi_value:
203-
conf.values[entry.key] = MultiValueConfigEntry.from_dict(entry.to_dict())
204-
else:
205-
conf.values[entry.key] = ConfigEntry.from_dict(entry.to_dict())
176+
conf.values[entry.key] = ConfigEntry.from_dict(entry.to_dict())
206177
conf.values[entry.key].parse_value(
207178
raw.get("values", {}).get(entry.key), allow_none=True
208179
)
@@ -212,8 +183,8 @@ def to_raw(self) -> dict[str, Any]:
212183
"""Return minimized/raw dict to store in persistent storage."""
213184

214185
def _handle_value(
215-
value: ConfigEntry | MultiValueConfigEntry,
216-
) -> ConfigValueTypes:
186+
value: ConfigEntry,
187+
) -> ConfigValueType:
217188
if value.type == ConfigEntryType.SECURE_STRING:
218189
assert isinstance(value.value, str)
219190
assert ENCRYPT_CALLBACK is not None
@@ -238,7 +209,7 @@ def __post_serialize__(self, d: dict[str, Any]) -> dict[str, Any]:
238209
d["values"][key]["value"] = SECURE_STRING_SUBSTITUTE
239210
return d
240211

241-
def update(self, update: dict[str, ConfigValueTypes]) -> set[str]:
212+
def update(self, update: dict[str, ConfigValueType]) -> set[str]:
242213
"""Update Config with updated values."""
243214
changed_keys: set[str] = set()
244215

@@ -261,7 +232,7 @@ def update(self, update: dict[str, ConfigValueTypes]) -> set[str]:
261232
continue
262233
cur_val = self.values[key].value if key in self.values else None
263234
# parse entry to do type validation
264-
parsed_val = self.values[key].parse_value(new_val) # type: ignore[arg-type]
235+
parsed_val = self.values[key].parse_value(new_val)
265236
if cur_val != parsed_val:
266237
changed_keys.add(f"values/{key}")
267238

@@ -272,7 +243,7 @@ def validate(self) -> None:
272243
# For now we just use the parse method to check for not allowed None values
273244
# this can be extended later
274245
for value in self.values.values():
275-
value.parse_value(value.value, allow_none=False) # type: ignore[arg-type]
246+
value.parse_value(value.value, allow_none=False)
276247

277248

278249
@dataclass

music_assistant_models/enums.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ class ConfigEntryType(StrEnum):
489489
INTEGER = "integer"
490490
FLOAT = "float"
491491
LABEL = "label"
492-
INTEGER_TUPLE = "integer_tuple"
492+
SPLITTED_STRING = "splitted_string"
493493
DIVIDER = "divider"
494494
ACTION = "action"
495495
ICON = "icon"

0 commit comments

Comments
 (0)