summaryrefslogtreecommitdiffstats
path: root/deluge/config.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2023-02-19 15:05:52 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2023-02-19 16:15:47 +0000
commitb686174b07bd56af4e5ffaa23c24f27f417fc305 (patch)
tree1ce335620d99341d94e88c159c0b9b0f6f0de5a0 /deluge/config.py
parentAdding debian version 2.0.3-4. (diff)
downloaddeluge-b686174b07bd56af4e5ffaa23c24f27f417fc305.tar.xz
deluge-b686174b07bd56af4e5ffaa23c24f27f417fc305.zip
Merging upstream version 2.1.1 (Closes: #1026291).
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'deluge/config.py')
-rw-r--r--deluge/config.py221
1 files changed, 110 insertions, 111 deletions
diff --git a/deluge/config.py b/deluge/config.py
index c852996..c5cb312 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,78 +38,66 @@ 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(s):
- """Find json objects in a string.
+def find_json_objects(text, decoder=json.JSONDecoder()):
+ """Find json objects in text.
Args:
- s (str): the string to find json objects in
+ text (str): The text to find json objects within.
Returns:
list: A list of tuples containing start and end locations of json
- objects in string `s`. e.g. [(start, end), ...]
+ objects in the text. e.g. [(start, end), ...]
+
"""
objects = []
- opens = 0
- start = s.find('{')
- offset = start
-
- if start < 0:
- return []
-
- quoted = False
- for index, c in enumerate(s[offset:]):
- if c == '"':
- quoted = not quoted
- elif quoted:
- continue
- elif c == '{':
- opens += 1
- elif c == '}':
- opens -= 1
- if opens == 0:
- objects.append((start, index + offset + 1))
- start = index + offset + 1
+ offset = 0
+ while True:
+ try:
+ start = text.index('{', offset)
+ except ValueError:
+ break
+
+ try:
+ __, index = decoder.raw_decode(text[start:])
+ except json.decoder.JSONDecodeError:
+ offset = start + 1
+ else:
+ offset = start + index
+ objects.append((start, offset))
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:
@@ -120,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}
@@ -137,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:
@@ -147,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
@@ -155,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.
@@ -167,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
@@ -179,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 (
- callLater,
- ) # pylint: disable=redefined-outer-name
+ # 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):
@@ -306,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 (
- callLater,
- ) # pylint: disable=redefined-outer-name
-
# 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.
@@ -361,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.
@@ -404,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
@@ -436,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):
@@ -459,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]
@@ -471,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
@@ -485,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
@@ -496,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
@@ -504,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:
@@ -556,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()