Anki/pylib/anki/config.py
Damien Elmes 676f4e74a8 store config in separate DB table
- mtime is tracked on each key individually, which will allow
merging of config changes when syncing in the future
- added col.(get|set|remove)_config()
- in order to support existing code that was mutating returned
values (eg col.conf["something"]["another"] = 5), the returned list/dict
will be automatically wrapped so that when the value is dropped, it
will save the mutated item back to the DB if it's changed. Code that
is fetching lists/dicts from the config like so:

col.conf["foo"]["bar"] = baz
col.setMod()

will continue to work in most case, but should be gradually updated to:

conf = col.get_config("foo")
conf["bar"] = baz
col.set_config("foo", conf)
2020-04-06 15:39:47 +10:00

121 lines
3.3 KiB
Python

# 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.
"""
from __future__ import annotations
import copy
import json
import weakref
from typing import Any
import anki
class ConfigManager:
def __init__(self, col: anki.storage._Collection):
self.col = col.weakref()
def get_immutable(self, key: str) -> Any:
s = self.col.backend.get_config_json(key)
if not s:
raise KeyError
return json.loads(s)
def set(self, key: str, val: Any) -> None:
self.col.backend.set_config_json(key, val)
def remove(self, key: str) -> None:
self.col.backend.remove_config(key)
# Legacy dict interface
#########################
def __getitem__(self, key):
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, value):
self.set(key, value)
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
def setdefault(self, key, default):
if key not in self:
self[key] = default
return self[key]
def __contains__(self, key):
try:
self.get_immutable(key)
return True
except KeyError:
return False
def __delitem__(self, key):
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, key, val):
self.key = key
self.conf = conf
self.orig = copy.deepcopy(val)
super().__init__(val)
def __del__(self):
cur = list(self)
conf = self.conf()
if conf and self.orig != cur:
conf[self.key] = cur
class WrappedDict(dict):
def __init__(self, conf, key, val):
self.key = key
self.conf = conf
self.orig = copy.deepcopy(val)
super().__init__(val)
def __del__(self):
cur = dict(self)
conf = self.conf()
if conf and self.orig != cur:
conf[self.key] = cur