feat: allow append list options

This commit is contained in:
2024-10-17 18:45:38 +03:00
parent f48993ccd5
commit 7c6c24a46d
10 changed files with 243 additions and 28 deletions

View File

@ -161,8 +161,8 @@ class Setup(Handler):
repository_server(str): url of the repository
"""
# allow_no_value=True is required because pacman uses boolean configuration in which just keys present
# (e.g. NoProgressBar) which will lead to exception
configuration = Configuration(allow_no_value=True)
# (e.g. NoProgressBar) which will lead to exception. allow_multi_key=False is set just for fun
configuration = Configuration(allow_no_value=True, allow_multi_key=False)
# preserve case
# stupid mypy thinks that it is impossible
configuration.optionxform = lambda optionstr: optionstr # type: ignore[method-assign]

View File

@ -25,6 +25,7 @@ from collections.abc import Callable
from pathlib import Path
from typing import Any, Self
from ahriman.core.configuration.configuration_multi_dict import ConfigurationMultiDict
from ahriman.core.configuration.shell_interpolator import ShellInterpolator
from ahriman.core.exceptions import InitializeError
from ahriman.models.repository_id import RepositoryId
@ -69,21 +70,27 @@ class Configuration(configparser.RawConfigParser):
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
converters: dict[str, Callable[[str], Any]] # typing guard
def __init__(self, allow_no_value: bool = False) -> None:
def __init__(self, allow_no_value: bool = False, allow_multi_key: bool = True) -> None:
"""
Args:
allow_no_value(bool, optional): copies :class:`configparser.RawConfigParser` behaviour. In case if it is set
to ``True``, the keys without values will be allowed (Default value = False)
allow_multi_key(bool, optional): if set to ``False``, then the default dictionary class will be used to
store keys internally. Otherwise, the special implementation will be used, which supports arrays
(Default value = True)
"""
configparser.RawConfigParser.__init__(
self,
dict_type=ConfigurationMultiDict if allow_multi_key else dict, # type: ignore[arg-type]
allow_no_value=allow_no_value,
strict=False,
empty_lines_in_values=not allow_multi_key,
interpolation=ShellInterpolator(),
converters={
"list": shlex.split,
"path": self._convert_path,
"pathlist": lambda value: [self._convert_path(element) for element in shlex.split(value)],
}
},
)
self.repository_id: RepositoryId | None = None

View File

@ -0,0 +1,91 @@
#
# Copyright (c) 2021-2024 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import Any
from ahriman.core.exceptions import OptionError
class ConfigurationMultiDict(dict[str, Any]):
"""
wrapper around :class:`dict` to handle multiple configuration keys as lists if they end with ``[]``.
Examples:
This class is designed to be used only with :class:`configparser.RawConfigParser` class, but idea is that
if the key ends with ``[]`` it will be treated as array and the result will be appended to the current value.
In addition, if the value is empty, then it will clear previous values, e.g.:
>>> data = ConfigurationMultiDict()
>>>
>>> data["single"] = "value" # append normal key
>>> print(data) # {"single": "value"}
>>>
>>> data["array[]"] = ["value1"] # append array value
>>> data["array[]"] = ["value2"]
>>> print(data) # {"single": "value", "array": ["value1 value2"]}
>>>
>>> data["array[]"] = [""] # clear previous values
>>> data["array[]"] = ["value3"]
>>> print(data) # {"single": "value", "array": ["value3"]}
"""
def _set_array_value(self, key: str, value: Any) -> None:
"""
set array value. If the key already exists in the dictionary, its value will be prepended to new value
Args:
key(str): key to insert
value(Any): value of the related key
Raises:
OptionError: if the key already exists in the dictionary, but not a single value list or a string
"""
match self.get(key):
case [current_value] | str(current_value): # type: ignore[misc]
value = f"{current_value} {value}"
case None:
pass
case other:
raise OptionError(other)
super().__setitem__(key, [value])
def __setitem__(self, key: str, value: Any) -> None:
"""
set ``key`` to ``value``. If the value equals to ``[""]`` (array with empty string), then the key
will be removed (as expected from :class:`configparser.RawConfigParser`). If the key ends with
``[]``, the value will be treated as an array and vice versa.
Args:
key(str): key to insert
value(Any): value of the related key
Raises:
OptionError: if ``key`` contains ``[]``, but not at the end of the string (e.g. ``prefix[]suffix``)
"""
real_key, is_key_array, remaining = key.partition("[]")
if remaining:
raise OptionError(key)
match value:
case [""]: # empty value key
self.pop(real_key, None)
case [array_value] if is_key_array: # update array value
self._set_array_value(real_key, array_value)
case _: # normal key
super().__setitem__(real_key, value)