add list config options

This commit is contained in:
2024-10-17 18:45:38 +03:00
parent eff5be9490
commit 153cf448af
5 changed files with 152 additions and 22 deletions

View File

@ -200,9 +200,9 @@ class Setup(Handler):
multilib(bool): add or do not multilib repository to the configuration
repository_server(str): url of the repository
"""
# allow_no_value=True is required because pacman uses boolean configuration in which just keys present
# allow_multi_key=False 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)
configuration = Configuration(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,22 +70,25 @@ 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_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,
allow_no_value=allow_no_value,
dict_type=ConfigurationMultiDict if allow_multi_key else dict,
strict=False,
empty_lines_in_values=False,
interpolation=ShellInterpolator(),
converters={
"list": shlex.split,
"path": self._convert_path,
"pathlist": lambda value: [self._convert_path(element) for element in shlex.split(value)],
}
)
},
) # type: ignore[call-overload]
self.repository_id: RepositoryId | None = None
self.path: Path | None = None

View File

@ -0,0 +1,85 @@
#
# 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:`collections.OrderedDict` 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(self, key: str, value: list[Any]) -> None:
"""
set array value. If the key already exists in the dictionary, it will be prepended to the value
Args:
key(str): key to insert
value(list[Any]): value of the related key
"""
current_value = self.get(key)
if current_value is not None:
value = [f"{left} {right}" for left, right in zip(current_value, value)]
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_array, remaining = key.partition("[]")
if remaining:
raise OptionError(key)
if value == [""]: # em[ty value key
if real_key in self:
del self[real_key]
elif is_array and isinstance(value, list): # update array value
self._set_array(real_key, value)
else: # normal key
super().__setitem__(real_key, value)

View File

@ -0,0 +1,44 @@
import pytest
from ahriman.core.configuration.configuration_multi_dict import ConfigurationMultiDict
from ahriman.core.exceptions import OptionError
def test_setitem_non_list() -> None:
"""
must insert not list correctly
"""
instance = ConfigurationMultiDict()
instance["key"] = "value"
assert instance["key"] == "value"
def test_setitem_remove() -> None:
"""
must remove key
"""
instance = ConfigurationMultiDict()
instance["key"] = "value"
instance["key"] = [""]
assert "key" not in instance
def test_setitem_array() -> None:
"""
must set array correctly
"""
instance = ConfigurationMultiDict()
instance["key[]"] = ["value1"]
instance["key[]"] = ["value2"]
assert instance["key"] == ["value1 value2"]
def test_setitem_exception() -> None:
"""
must raise exception on invalid key
"""
instance = ConfigurationMultiDict()
with pytest.raises(OptionError):
instance["prefix[]suffix"] = "value"

View File

@ -60,7 +60,7 @@ target = console
[email]
host = 127.0.0.1
link_path =
link_path = http://example.com
no_empty_report = no
port = 587
receivers = mail@example.com
@ -72,9 +72,8 @@ templates = ../web/templates
use_utf = yes
[html]
path =
homepage =
link_path =
link_path = http://example.com
path = local/path
template = repo-index.jinja2
templates = ../web/templates
@ -82,17 +81,15 @@ templates = ../web/templates
manual = yes
[rss]
path =
homepage =
link_path =
link_path = http://example.com
path = local/path
template = rss.jinja2
templates = ../web/templates
[telegram]
api_key = apikey
api_key = api_key
chat_id = @ahrimantestchat
homepage =
link_path =
link_path = http://example.com
template = telegram-index.jinja2
templates = ../web/templates
@ -101,20 +98,20 @@ target =
[rsync]
command = rsync --archive --verbose --compress --partial --delete
remote =
remote = remote@example.com
[disabled]
[customs3]
type = s3
access_key =
access_key = access_key
bucket = bucket
region = eu-central-1
secret_key =
secret_key = secret_key
[github:x86_64]
owner = arcan1s
password =
password = pa55w0rd
repository = ahriman
username = arcan1s