[Config] Refactor config class

* Refactored duplication with setting config key and logging
* Simplified lazy importing reactor for callLater. This lazy importing
is required for testing and also prevents Gtk UI lockup if reactor
imported in Config.
* Fixed saving config to file when setting a key that doesn't exist yet.
This was due to returning early in the set_item method.
* Added a `default` arg to set_item to prevent saving to file when only
setting a default value for a key in init.
* Moved casting value to existing key type from set_item to dedicated
function.
This commit is contained in:
Calum Lind 2022-02-05 15:47:01 +00:00
parent 8b0c8392b6
commit a684029602
No known key found for this signature in database
GPG Key ID: 90597A687B836BA3
2 changed files with 45 additions and 46 deletions

View File

@ -49,7 +49,6 @@ from tempfile import NamedTemporaryFile
from deluge.common import JSON_FORMAT, get_default_config_dir from deluge.common import JSON_FORMAT, get_default_config_dir
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
callLater = None # noqa: N816 Necessary for the config tests
def prop(func): def prop(func):
@ -100,6 +99,21 @@ def find_json_objects(text, decoder=json.JSONDecoder()):
return objects return objects
def cast_to_existing_type(value, old_value):
"""Attempt to convert new value type to match old value type"""
types_match = isinstance(old_value, (type(None), type(value)))
if value is not None and not types_match:
old_type = type(old_value)
# Skip convert to bytes since requires knowledge of encoding and value should
# be unicode anyway.
if old_type is bytes:
return value
return old_type(value)
return value
class Config: class Config:
"""This class is used to access/create/modify config files. """This class is used to access/create/modify config files.
@ -127,7 +141,7 @@ class Config:
if defaults: if defaults:
for key, value in defaults.items(): for key, value in defaults.items():
self.set_item(key, value) self.set_item(key, value, default=True)
# Load the config from file in the config_dir # Load the config from file in the config_dir
if config_dir: if config_dir:
@ -137,6 +151,12 @@ class Config:
self.load() self.load()
def callLater(self, period, func): # noqa: N802 ignore camelCase
"""Wrapper around reactor.callLater for test purpose."""
from twisted.internet import reactor
return reactor.callLater(period, func)
def __contains__(self, item): def __contains__(self, item):
return item in self.__config return item in self.__config
@ -145,7 +165,7 @@ class Config:
return self.set_item(key, value) return self.set_item(key, value)
def set_item(self, key, value): def set_item(self, key, value, default=False):
"""Sets item 'key' to 'value' in the config dictionary. """Sets item 'key' to 'value' in the config dictionary.
Does not allow changing the item's type unless it is None. Does not allow changing the item's type unless it is None.
@ -157,6 +177,8 @@ class Config:
key (str): Item to change to change. key (str): Item to change to change.
value (any): The value to change item to, must be same type as what is value (any): The value to change item to, must be same type as what is
currently in the config. currently in the config.
default (optional, bool): When setting a default value skip func or save
callbacks.
Raises: Raises:
ValueError: Raised when the type of value is not the same as what is ValueError: Raised when the type of value is not the same as what is
@ -169,58 +191,43 @@ class Config:
5 5
""" """
if key not in self.__config: if isinstance(value, bytes):
self.__config[key] = value value = value.decode()
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
return
if self.__config[key] == value: if key in self.__config:
return
# Change the value type if it is not None and does not match.
type_match = isinstance(self.__config[key], (type(None), type(value)))
if value is not None and not type_match:
try: try:
oldtype = type(self.__config[key]) value = cast_to_existing_type(value, self.__config[key])
# Don't convert to bytes as requires encoding and value will
# be decoded anyway.
if oldtype is not bytes:
value = oldtype(value)
except ValueError: except ValueError:
log.warning('Value Type "%s" invalid for key: %s', type(value), key) log.warning('Value Type "%s" invalid for key: %s', type(value), key)
raise raise
else:
if isinstance(value, bytes): if self.__config[key] == value:
value = value.decode('utf8') return
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value)) log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
self.__config[key] = value self.__config[key] = value
global callLater # Skip save or func callbacks if setting default value for keys
if callLater is None: if default:
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError return
from twisted.internet.reactor import ( # pylint: disable=redefined-outer-name
callLater,
)
# Run the set_function for this key if any # Run the set_function for this key if any
try: for func in self.__set_functions.get(key, []):
for func in self.__set_functions[key]: self.callLater(0, func, key, value)
callLater(0, func, key, value)
except KeyError:
pass
try: try:
def do_change_callbacks(key, value): def do_change_callbacks(key, value):
for func in self.__change_callbacks: for func in self.__change_callbacks:
func(key, value) func(key, value)
callLater(0, do_change_callbacks, key, value) self.callLater(0, do_change_callbacks, key, value)
except Exception: except Exception:
pass pass
# We set the save_timer for 5 seconds if not already set # We set the save_timer for 5 seconds if not already set
if not self._save_timer or not self._save_timer.active(): if not self._save_timer or not self._save_timer.active():
self._save_timer = callLater(5, self.save) self._save_timer = self.callLater(5, self.save)
def __getitem__(self, key): def __getitem__(self, key):
"""See get_item """ """See get_item """
@ -296,16 +303,9 @@ class Config:
del self.__config[key] del self.__config[key]
global callLater
if callLater is None:
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError
from twisted.internet.reactor import ( # pylint: disable=redefined-outer-name
callLater,
)
# We set the save_timer for 5 seconds if not already set # We set the save_timer for 5 seconds if not already set
if not self._save_timer or not self._save_timer.active(): if not self._save_timer or not self._save_timer.active():
self._save_timer = callLater(5, self.save) self._save_timer = self.callLater(5, self.save)
def register_change_callback(self, callback): def register_change_callback(self, callback):
"""Registers a callback function for any changed value. """Registers a callback function for any changed value.

View File

@ -10,7 +10,6 @@ from codecs import getwriter
import pytest import pytest
from twisted.internet import task from twisted.internet import task
import deluge.config
from deluge.common import JSON_FORMAT from deluge.common import JSON_FORMAT
from deluge.config import Config from deluge.config import Config
@ -136,16 +135,16 @@ class TestConfig:
assert config['int'] == 2 assert config['int'] == 2
def test_save_timer(self): def test_save_timer(self):
self.clock = task.Clock() clock = task.Clock()
deluge.config.callLater = self.clock.callLater
config = Config('test.conf', defaults=DEFAULTS, config_dir=self.config_dir) config = Config('test.conf', defaults=DEFAULTS, config_dir=self.config_dir)
config.callLater = clock.callLater
config['string'] = 'baz' config['string'] = 'baz'
config['int'] = 2 config['int'] = 2
assert config._save_timer.active() assert config._save_timer.active()
# Timeout set for 5 seconds in config, so lets move clock by 5 seconds # Timeout set for 5 seconds in config, so lets move clock by 5 seconds
self.clock.advance(5) clock.advance(5)
def check_config(config): def check_config(config):
assert not config._save_timer.active() assert not config._save_timer.active()