diff --git a/src/kleinanzeigen_bot/utils/dicts.py b/src/kleinanzeigen_bot/utils/dicts.py index f33b153..3709f7d 100644 --- a/src/kleinanzeigen_bot/utils/dicts.py +++ b/src/kleinanzeigen_bot/utils/dicts.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/ import copy, json, os # isort: skip +from collections import defaultdict from collections.abc import Callable from gettext import gettext as _ from importlib.resources import read_text as get_resource_as_string @@ -12,6 +13,7 @@ from typing import Any, Final from ruamel.yaml import YAML from . import files, loggers # pylint: disable=cyclic-import +from .misc import K, V LOG:Final[loggers.Logger] = loggers.get_logger(__name__) @@ -23,30 +25,50 @@ def apply_defaults( override:Callable[[Any, Any], bool] = lambda _k, _v: False ) -> dict[Any, Any]: """ - >>> apply_defaults({}, {"foo": "bar"}) - {'foo': 'bar'} - >>> apply_defaults({"foo": "foo"}, {"foo": "bar"}) - {'foo': 'foo'} - >>> apply_defaults({"foo": ""}, {"foo": "bar"}) - {'foo': ''} - >>> apply_defaults({}, {"foo": "bar"}, ignore = lambda k, _: k == "foo") + >>> apply_defaults({}, {'a': 'b'}) + {'a': 'b'} + >>> apply_defaults({'a': 'b'}, {'a': 'c'}) + {'a': 'b'} + >>> apply_defaults({'a': ''}, {'a': 'b'}) + {'a': ''} + >>> apply_defaults({}, {'a': 'b'}, ignore = lambda k, _: k == 'a') {} - >>> apply_defaults({"foo": ""}, {"foo": "bar"}, override = lambda _, v: v == "") - {'foo': 'bar'} - >>> apply_defaults({"foo": None}, {"foo": "bar"}, override = lambda _, v: v == "") - {'foo': None} + >>> apply_defaults({'a': ''}, {'a': 'b'}, override = lambda _, v: v == '') + {'a': 'b'} + >>> apply_defaults({'a': None}, {'a': 'b'}, override = lambda _, v: v == '') + {'a': None} + >>> apply_defaults({'a': {'x': 1}}, {'a': {'x': 0, 'y': 2}}) + {'a': {'x': 1, 'y': 2}} + >>> apply_defaults({'a': {'b': False}}, {'a': { 'b': True}}) + {'a': {'b': False}} """ for key, default_value in defaults.items(): if key in target: if isinstance(target[key], dict) and isinstance(default_value, dict): - apply_defaults(target[key], default_value, ignore = ignore) - elif override(key, target[key]): + apply_defaults( + target = target[key], + defaults = default_value, + ignore = ignore, + override = override + ) + elif override(key, target[key]): # force overwrite if override says so target[key] = copy.deepcopy(default_value) - elif not ignore(key, default_value): + elif not ignore(key, default_value): # only set if not explicitly ignored target[key] = copy.deepcopy(default_value) return target +def defaultdict_to_dict(d: defaultdict[K, V]) -> dict[K, V]: + """Recursively convert defaultdict to dict.""" + result: dict[K, V] = {} + for key, value in d.items(): + if isinstance(value, defaultdict): + result[key] = defaultdict_to_dict(value) # type: ignore[assignment] + else: + result[key] = value + return result + + def load_dict(filepath:str, content_label:str = "") -> dict[str, Any]: """ :raises FileNotFoundError diff --git a/src/kleinanzeigen_bot/utils/misc.py b/src/kleinanzeigen_bot/utils/misc.py index f377d27..2c92b4b 100644 --- a/src/kleinanzeigen_bot/utils/misc.py +++ b/src/kleinanzeigen_bot/utils/misc.py @@ -11,6 +11,8 @@ from . import i18n # https://mypy.readthedocs.io/en/stable/generics.html#generic-functions T = TypeVar("T") +K = TypeVar("K") +V = TypeVar("V") def ensure(