summaryrefslogtreecommitdiffstats
path: root/deluge/config.py
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/config.py')
-rw-r--r--deluge/config.py178
1 files changed, 91 insertions, 87 deletions
diff --git a/deluge/config.py b/deluge/config.py
index 80b30faf5..c5cb3122b 100644
--- a/deluge/config.py
+++ b/deluge/config.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
#
@@ -39,39 +38,17 @@ this can only be done for the 'config file version' and not for the 'format'
version as this will be done internally.
"""
-from __future__ import unicode_literals
-
import json
import logging
import os
+import pickle
import shutil
from codecs import getwriter
-from io import open
from tempfile import NamedTemporaryFile
-import six.moves.cPickle as pickle # noqa: N813
-
from deluge.common import JSON_FORMAT, get_default_config_dir
log = logging.getLogger(__name__)
-callLater = None # noqa: N816 Necessary for the config tests
-
-
-def prop(func):
- """Function decorator for defining property attributes
-
- The decorated function is expected to return a dictionary
- containing one or more of the following pairs:
-
- fget - function for getting attribute value
- fset - function for setting attribute value
- fdel - function for deleting attribute
-
- This can be conveniently constructed by the locals() builtin
- function; see:
- http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183
- """
- return property(doc=func.__doc__, **func())
def find_json_objects(text, decoder=json.JSONDecoder()):
@@ -105,7 +82,22 @@ def find_json_objects(text, decoder=json.JSONDecoder()):
return objects
-class Config(object):
+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:
"""This class is used to access/create/modify config files.
Args:
@@ -115,13 +107,23 @@ class Config(object):
file_version (int): The file format for the default config values when creating
a fresh config. This value should be increased whenever a new migration function is
setup to convert old config files. (default: 1)
+ log_mask_funcs (dict): A dict of key:function, used to mask sensitive
+ key values (e.g. passwords) when logging is enabled.
"""
- def __init__(self, filename, defaults=None, config_dir=None, file_version=1):
+ def __init__(
+ self,
+ filename,
+ defaults=None,
+ config_dir=None,
+ file_version=1,
+ log_mask_funcs=None,
+ ):
self.__config = {}
self.__set_functions = {}
self.__change_callbacks = []
+ self.__log_mask_funcs = log_mask_funcs if log_mask_funcs else {}
# These hold the version numbers and they will be set when loaded
self.__version = {'format': 1, 'file': file_version}
@@ -132,7 +134,7 @@ class Config(object):
if defaults:
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
if config_dir:
@@ -142,6 +144,12 @@ class Config(object):
self.load()
+ def callLater(self, period, func, *args, **kwargs): # noqa: N802 ignore camelCase
+ """Wrapper around reactor.callLater for test purpose."""
+ from twisted.internet import reactor
+
+ return reactor.callLater(period, func, *args, **kwargs)
+
def __contains__(self, item):
return item in self.__config
@@ -150,7 +158,7 @@ class Config(object):
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.
Does not allow changing the item's type unless it is None.
@@ -162,6 +170,8 @@ class Config(object):
key (str): Item to change to change.
value (any): The value to change item to, must be same type as what is
currently in the config.
+ default (optional, bool): When setting a default value skip func or save
+ callbacks.
Raises:
ValueError: Raised when the type of value is not the same as what is
@@ -174,61 +184,54 @@ class Config(object):
5
"""
- if key not in self.__config:
- self.__config[key] = value
- log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
- return
-
- if self.__config[key] == value:
- return
+ if isinstance(value, bytes):
+ value = value.decode()
- # 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:
+ if key in self.__config:
try:
- oldtype = type(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)
+ value = cast_to_existing_type(value, self.__config[key])
except ValueError:
log.warning('Value Type "%s" invalid for key: %s', type(value), key)
raise
+ else:
+ if self.__config[key] == value:
+ return
- if isinstance(value, bytes):
- value = value.decode('utf8')
-
- log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
+ if log.isEnabledFor(logging.DEBUG):
+ if key in self.__log_mask_funcs:
+ value = self.__log_mask_funcs[key](value)
+ log.debug(
+ 'Setting key "%s" to: %s (of type: %s)',
+ key,
+ value,
+ type(value),
+ )
self.__config[key] = value
- 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,
- )
+ # Skip save or func callbacks if setting default value for keys
+ if default:
+ return
+
# Run the set_function for this key if any
- try:
- for func in self.__set_functions[key]:
- callLater(0, func, key, value)
- except KeyError:
- pass
+ for func in self.__set_functions.get(key, []):
+ self.callLater(0, func, key, value)
+
try:
def do_change_callbacks(key, value):
for func in self.__change_callbacks:
func(key, value)
- callLater(0, do_change_callbacks, key, value)
+ self.callLater(0, do_change_callbacks, key, value)
except Exception:
pass
# We set the save_timer for 5 seconds if not already set
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):
- """See get_item """
+ """See get_item"""
return self.get_item(key)
def get_item(self, key):
@@ -301,16 +304,9 @@ class Config(object):
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
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):
"""Registers a callback function for any changed value.
@@ -356,7 +352,6 @@ class Config(object):
# Run the function now if apply_now is set
if apply_now:
function(key, self.__config[key])
- return
def apply_all(self):
"""Calls all set functions.
@@ -399,9 +394,9 @@ class Config(object):
filename = self.__config_file
try:
- with open(filename, 'r', encoding='utf8') as _file:
+ with open(filename, encoding='utf8') as _file:
data = _file.read()
- except IOError as ex:
+ except OSError as ex:
log.warning('Unable to open config file %s: %s', filename, ex)
return
@@ -431,12 +426,24 @@ class Config(object):
log.exception(ex)
log.warning('Unable to load config file: %s', filename)
+ if not log.isEnabledFor(logging.DEBUG):
+ return
+
+ config = self.__config
+ if self.__log_mask_funcs:
+ config = {
+ key: self.__log_mask_funcs[key](config[key])
+ if key in self.__log_mask_funcs
+ else config[key]
+ for key in config
+ }
+
log.debug(
'Config %s version: %s.%s loaded: %s',
filename,
self.__version['format'],
self.__version['file'],
- self.__config,
+ config,
)
def save(self, filename=None):
@@ -454,7 +461,7 @@ class Config(object):
# Check to see if the current config differs from the one on disk
# We will only write a new config file if there is a difference
try:
- with open(filename, 'r', encoding='utf8') as _file:
+ with open(filename, encoding='utf8') as _file:
data = _file.read()
objects = find_json_objects(data)
start, end = objects[0]
@@ -466,7 +473,7 @@ class Config(object):
if self._save_timer and self._save_timer.active():
self._save_timer.cancel()
return True
- except (IOError, IndexError) as ex:
+ except (OSError, IndexError) as ex:
log.warning('Unable to open config file: %s because: %s', filename, ex)
# Save the new config and make sure it's written to disk
@@ -480,7 +487,7 @@ class Config(object):
json.dump(self.__config, getwriter('utf8')(_file), **JSON_FORMAT)
_file.flush()
os.fsync(_file.fileno())
- except IOError as ex:
+ except OSError as ex:
log.error('Error writing new config file: %s', ex)
return False
@@ -491,7 +498,7 @@ class Config(object):
try:
log.debug('Backing up old config file to %s.bak', filename)
shutil.move(filename, filename + '.bak')
- except IOError as ex:
+ except OSError as ex:
log.warning('Unable to backup old config: %s', ex)
# The new config file has been written successfully, so let's move it over
@@ -499,7 +506,7 @@ class Config(object):
try:
log.debug('Moving new config file %s to %s', filename_tmp, filename)
shutil.move(filename_tmp, filename)
- except IOError as ex:
+ except OSError as ex:
log.error('Error moving new config file: %s', ex)
return False
else:
@@ -551,14 +558,11 @@ class Config(object):
def config_file(self):
return self.__config_file
- @prop
- def config(): # pylint: disable=no-method-argument
+ @property
+ def config(self):
"""The config dictionary"""
+ return self.__config
- def fget(self):
- return self.__config
-
- def fdel(self):
- return self.save()
-
- return locals()
+ @config.deleter
+ def config(self):
+ return self.save()