# Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html """ Config handling - To set a config value, use col.set_config(key, val). - To get a config value, use col.get_config(key, default=None). In the case of lists and dictionaries, any changes you make to the returned value will not be saved unless you call set_config(). - To remove a config value, use col.remove_config(key). For legacy reasons, the config is also exposed as a dict interface as col.conf. To support old code that was mutating inner values, using col.conf["key"] needs to wrap lists and dicts when returning them. As this is less efficient, please use the col.*_config() API in new code. The legacy set also does not support the new undo handling. """ from __future__ import annotations import copy import weakref from typing import Any from weakref import ref import anki import anki.collection from anki import config_pb2 from anki.collection import OpChanges from anki.errors import NotFoundError from anki.utils import from_json_bytes, to_json_bytes Config = config_pb2.ConfigKey class ConfigManager: def __init__(self, col: anki.collection.Collection): self.col = col.weakref() def get_immutable(self, key: str) -> Any: try: return from_json_bytes(self.col._backend.get_config_json(key)) except NotFoundError as exc: raise KeyError from exc def set(self, key: str, val: Any) -> None: self.col._backend.set_config_json_no_undo( key=key, value_json=to_json_bytes(val), # this argument is ignored undoable=True, ) def remove(self, key: str) -> OpChanges: return self.col._backend.remove_config(key) # Legacy dict interface ######################### def __getitem__(self, key: str) -> Any: val = self.get_immutable(key) if isinstance(val, list): print( f"conf key {key} should be fetched with col.get_config(), and saved with col.set_config()" ) return WrappedList(weakref.ref(self), key, val) elif isinstance(val, dict): print( f"conf key {key} should be fetched with col.get_config(), and saved with col.set_config()" ) return WrappedDict(weakref.ref(self), key, val) else: return val def __setitem__(self, key: str, value: Any) -> None: self.set(key, value) def get(self, key: str, default: Any | None = None) -> Any: try: return self[key] except KeyError: return default def setdefault(self, key: str, default: Any) -> Any: if key not in self: self[key] = default return self[key] def __contains__(self, key: str) -> bool: try: self.get_immutable(key) return True except KeyError: return False def __delitem__(self, key: str) -> None: self.remove(key) # Tracking changes to mutable objects ######################################### # Because we previously allowed mutation of the conf # structure directly, to allow col.conf["foo"]["bar"] = xx # to continue to function, we apply changes as the object # is dropped. class WrappedList(list): def __init__(self, conf: ref[ConfigManager], key: str, val: Any) -> None: self.key = key self.conf = conf self.orig = copy.deepcopy(val) super().__init__(val) def __del__(self) -> None: cur = list(self) conf = self.conf() if conf and self.orig != cur: conf[self.key] = cur class WrappedDict(dict): def __init__(self, conf: ref[ConfigManager], key: str, val: Any) -> None: self.key = key self.conf = conf self.orig = copy.deepcopy(val) super().__init__(val) def __del__(self) -> None: cur = dict(self) conf = self.conf() if conf and self.orig != cur: conf[self.key] = cur