# Back In Time
# Copyright (C) 2008-2022 Oprea Dan, Bart de Koning, Richard Bailey, Germar
# Reitze
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import os
import collections
import re
import logger
[docs]
class ConfigFile(object):
"""Store options in a plain text file in form of: key=value
"""
def __init__(self):
self.dict = {}
self.errorHandler = None
self.questionHandler = None
[docs]
def setErrorHandler(self, handler):
"""
Register a function that should be called for notifying errors.
handler (method): callable function
"""
self.errorHandler = handler
[docs]
def setQuestionHandler(self, handler):
"""
Register a function that should be called for asking questions.
handler (method): callable function
"""
self.questionHandler = handler
[docs]
def clearHandlers(self):
"""
Reset error and question handlers.
"""
self.errorHandler = None
self.questionHandler = None
[docs]
def notifyError(self, message):
"""
Call previously registered function to show an error.
Args:
message (str): error message that should be shown
"""
if self.errorHandler is None:
return
self.errorHandler(message)
[docs]
def askQuestion(self, message):
"""
Call previously registered function to ask a question.
Args:
message (str): question that should be shown
"""
if self.questionHandler is None:
return False
return self.questionHandler(message)
[docs]
def save(self, filename):
"""
Save all options to file.
Args:
filename (str): full path
Returns:
bool: ``True`` if successful
"""
def numsort(key):
"""
Sort int in keys in numeric order instead of alphabetical by adding
leading zeros to int's
"""
return re.sub(r'\d+', lambda m: m.group(0).zfill(6), key)
try:
with open(filename, 'wt') as f:
keys = list(self.dict.keys())
keys.sort(key=numsort)
for key in keys:
f.write("%s=%s\n" % (key, self.dict[key]))
except OSError as e:
logger.error('Failed to save config: %s' % str(e), self)
self.notifyError(
'{}: {}'.format(_('Failed to save config'), str(e)))
return False
return True
[docs]
def load(self, filename, **kwargs):
"""
Reset current options and load new options from file.
Args:
filename (str): full path
"""
self.dict = {}
self.append(filename, **kwargs)
[docs]
def append(self, filename, maxsplit=1):
"""
Load options from file and append them to current options.
Args:
filename (str): full path
maxsplit (int): split lines only n times on '='
"""
lines = []
if not os.path.isfile(filename):
return
try:
with open(filename, 'rt') as f:
lines = f.readlines()
except OSError as e:
logger.error('Failed to load config: %s' % str(e), self)
self.notifyError(
'{}: {}'.format(_('Failed to load config'), str(e)))
for line in lines:
items = line.strip('\n').split('=', maxsplit)
if len(items) == 2:
self.dict[items[0]] = items[1]
[docs]
def remapKey(self, old_key, new_key):
"""
Remap keys to a new key name.
Args:
old_key (str): old key name
new_key (str): new key name
"""
if old_key != new_key:
if old_key in self.dict:
if new_key not in self.dict:
self.dict[new_key] = self.dict[old_key]
del self.dict[old_key]
[docs]
def remapKeyRegex(self, pattern, replace):
"""
Remap keys to a new key name using :py:func:`re.sub`.
Args:
pattern (str): part of key name that should be replaced
replace (:py:class:`str`, method):
string or a callable function which will be used
to replace all matches of ``pattern``.
"""
c = re.compile(pattern)
for key in list(self.dict):
newKey = c.sub(replace, key)
if key != newKey:
self.remapKey(key, newKey)
[docs]
def hasKey(self, key):
"""
``True`` if key is set.
Args:
key (str): string used as key
Returns:
bool: ``True`` if the ``key`` is set
"""
return key in self.dict
[docs]
def strValue(self, key, default=''):
"""
Return a 'str' instance of key's value.
Args:
key (str): Key identifying the value in the config file.
default (str): Default value if ``key`` is not present.
Returns:
str: Value of ``key`` or ``default``.
"""
if key in self.dict:
return self.dict[key]
else:
return default
[docs]
def setStrValue(self, key, value):
"""
Set a string value for key.
Args:
key (str): string used as key
value (str): store this value
"""
self.dict[key] = value
[docs]
def intValue(self, key, default=0):
"""
Return a 'int' instance of key's value.
Args:
key (str): string used as key
default (int): return this if ``key`` is not set
Returns:
int: value of ``key`` or ``default``
if ``key`` is not set.
"""
try:
return int(self.dict[key])
except Exception:
return default
[docs]
def setIntValue(self, key, value):
"""
Set an integer value for key.
Args:
key (str): string used as key
value (int): store this option
"""
self.setStrValue(key, str(value))
[docs]
def boolValue(self, key, default=False):
"""
Return a 'bool' instance of key's value.
Args:
key (str): string used as key
default (bool): return this if key is not set
Returns:
bool: value of 'key' or 'default'
if 'key' is not set.
"""
try:
val = self.dict[key]
if "1" == val or "TRUE" == val.upper():
return True
return False
except Exception:
return default
[docs]
def setBoolValue(self, key, value):
"""
Set a bool value for key.
Args:
key (str): string used as key
value (bool): store this option
"""
if value:
self.setStrValue(key, 'true')
else:
self.setStrValue(key, 'false')
[docs]
def listValue(self, key, type_key='str:value', default=[]):
"""
Return a list of values
Size of the list must be stored in key.size
Args:
key (str): used base-key
type_key (str): pattern of 'value-type:value-name'.
See examples below.
default (list): default value
Returns:
list: value of ``key`` or ``default``
if ``key`` is not set.
``type_key`` pattern examples::
'str:value' => return str values from key.value
'int:type' => return int values from key.type
'bool:enabled' => return bool values from key.enabled
('str:value', 'int:type') => return tuple of values
"""
def typeKeySplit(tk):
t, k = '', ''
if isinstance(tk, str):
t, k = tk.split(':', maxsplit=1)
return (t, k)
def value(key, tk):
t, k = typeKeySplit(tk)
if t in ('str', 'int', 'bool'):
func = getattr(self, '%sValue' % t)
return func('%s.%s' % (key, k))
raise TypeError('Invalid type_key: %s' % tk)
size = self.intValue('%s.size' % key, -1)
if size < 0:
return default
ret = []
for i in range(1, size + 1):
if isinstance(type_key, str):
if not self.hasKey('%s.%s.%s' %
(key, i, typeKeySplit(type_key)[1])):
continue
ret.append(value('%s.%s' % (key, i), type_key))
elif isinstance(type_key, tuple):
if not self.hasKey('%s.%s.%s' %
(key, i, typeKeySplit(type_key[0])[1])):
continue
items = []
for tk in type_key:
items.append(value('%s.%s' % (key, i), tk))
ret.append(tuple(items))
else:
raise TypeError('Invalid type_key: %s' % type_key)
return ret
[docs]
def setListValue(self, key, type_key, value):
"""
Set a list of values.
Size of the list will be stored in key.size
Args:
key (str): used base-key
type_key (str): pattern of 'value-type:value-name'.
See examples below.
value (list): that should be stored
``type_key`` pattern examples::
'str:value' => return str values from key.value
'int:type' => return int values from key.type
'bool:enabled' => return bool values from key.enabled
('str:value', 'int:type') => return tuple of values
"""
def setValue(key, tk, v):
t = ''
if isinstance(tk, str):
t, k = tk.split(':', maxsplit=1)
if t in ('str', 'int', 'bool'):
func = getattr(self, 'set%sValue' % t.capitalize())
return func('%s.%s' % (key, k), v)
raise TypeError('Invalid type_key: %s' % tk)
if not isinstance(value, (list, tuple)):
raise TypeError('value has wrong type: %s' % value)
old_size = self.intValue('%s.size' % key, -1)
self.setIntValue('%s.size' % key, len(value))
for i, v in enumerate(value, start=1):
if isinstance(type_key, str):
setValue('%s.%s' % (key, i), type_key, v)
elif isinstance(type_key, tuple):
for iv, tk in enumerate(type_key):
if len(v) > iv:
setValue('%s.%s' % (key, i), tk, v[iv])
else:
self.removeKey('%s.%s.%s' % (key, i, tk.split(':')[1]))
else:
raise TypeError('Invalid type_key: %s' % type_key)
if len(value) < old_size:
for i in range(len(value) + 1, old_size + 1):
if isinstance(type_key, str):
self.removeKey(
'%s.%s.%s' % (key, i, type_key.split(':')[1]))
elif isinstance(type_key, tuple):
for tk in type_key:
self.removeKey(
'%s.%s.%s' % (key, i, tk.split(':')[1]))
[docs]
def removeKey(self, key):
"""
Remove key from options.
Args:
key (str): string used as key
"""
if key in self.dict:
del self.dict[key]
[docs]
def removeKeysStartsWith(self, prefix):
"""
Remove key from options which start with given prefix.
Args:
prefix (str): prefix for keys (key starts with this string)
that should be removed
"""
removeKeys = []
for key in self.dict.keys():
if key.startswith(prefix):
removeKeys.append(key)
for key in removeKeys:
del self.dict[key]
[docs]
def keys(self):
return list(self.dict.keys())
[docs]
class ConfigFileWithProfiles(ConfigFile):
"""
Store options in profiles as 'profileX.key=value'
Args:
default_profile_name (str): default name of the first profile.
"""
def __init__(self, default_profile_name=''):
ConfigFile.__init__(self)
self.default_profile_name = default_profile_name
self.current_profile_id = '1'
self.setCurrentProfile(self.current_profile_id)
[docs]
def load(self, filename):
"""
Reset current options and load new options from file.
Args:
filename (str): full path
"""
self.current_profile_id = '1'
super(ConfigFileWithProfiles, self).load(filename)
[docs]
def append(self, filename):
"""
Load options from file and append them to current options.
Args:
filename (str): full path
"""
super(ConfigFileWithProfiles, self).append(filename)
found = False
profiles = self.profiles()
for profile_id in profiles:
if profile_id == self.current_profile_id:
found = True
break
if not found and profiles:
self.current_profile_id = profiles[0]
if self.intValue('profiles.version') <= 0:
rename_keys = []
for key in self.dict.keys():
if key.startswith('profile.0.'):
rename_keys.append(key)
for old_key in rename_keys:
new_key = 'profile1.' + old_key[10:]
self.dict[new_key] = self.dict[old_key]
del self.dict[old_key]
if self.intValue('profiles.version') != 1:
self.setIntValue('profiles.version', 1)
[docs]
def profiles(self):
"""
List of all available profile IDs. Profile IDs are strings!
Returns:
list: all available profile IDs as strings
"""
return self.strValue('profiles', '1').split(':')
[docs]
def profilesSortedByName(self):
"""
List of available profile IDs alphabetically sorted by their names.
Profile IDs are strings!
Returns:
list: all available profile IDs as strings
"""
profiles_unsorted = self.profiles()
if len(profiles_unsorted) <= 1:
return profiles_unsorted
profiles_dict = {}
for profile_id in profiles_unsorted:
profiles_dict[self.profileName(profile_id).upper()] = profile_id
# sort the dictionary by key (the profile name)
profiles_sorted = collections.OrderedDict(
sorted(profiles_dict.items()))
# return the names as a list
return list(profiles_sorted.values())
[docs]
def currentProfile(self):
"""
Currently selected profile ID. Profile IDs are strings!
Returns:
str: profile ID
"""
return self.current_profile_id
[docs]
def setCurrentProfile(self, profile_id):
"""
Change the current profile.
Args:
profile_id (str, int): valid profile ID
Returns:
bool: ``True`` if successful
"""
if isinstance(profile_id, int):
profile_id = str(profile_id)
profiles = self.profiles()
for i in profiles:
if i == profile_id:
profile_name = self.profileName(profile_id)
self.current_profile_id = profile_id
logger.changeProfile(profile_id, profile_name)
logger.debug(
f'Change current profile: {profile_id}={profile_name}',
self)
return True
return False
[docs]
def setCurrentProfileByName(self, name):
"""
Change the current profile by a givin name.
Args:
name (str): valid profile name
Returns:
bool: ``True`` if successful
"""
# Find the profile_id to this name...
for profile_id in self.profiles():
if self.profileName(profile_id) == name:
# ...and set current profile by this id.
return self.setCurrentProfile(profile_id)
return False
[docs]
def profileExists(self, profile_id):
"""
``True`` if the profile exists.
Args:
profile_id (str, int): profile ID
Returns:
bool: ``True`` if ``profile_id`` exists.
"""
if isinstance(profile_id, int):
profile_id = str(profile_id)
return profile_id in self.profiles()
[docs]
def profileExistsByName(self, name):
"""
``True`` if the profile exists.
Args:
name (str): profile name
Returns:
bool: ``True`` if ``name`` exists.
"""
profiles = self.profiles()
for profile_id in profiles:
if self.profileName(profile_id) == name:
return True
return False
[docs]
def profileName(self, profile_id=None):
"""
Name of the profile.
Args:
profile_id (str, int): valid profile ID
Returns:
str: name of profile
"""
if isinstance(profile_id, int):
profile_id = str(profile_id)
if profile_id is None:
profile_id = self.current_profile_id
if profile_id == '1':
default = self.default_profile_name
else:
default = 'Profile %s' % profile_id
return self.profileStrValue('name', default, profile_id)
[docs]
def addProfile(self, name):
"""
Add a new profile if the name is not already in use.
Args:
name (str): new profile name
Returns:
str: new profile ID
"""
profiles = self.profiles()
for profile_id in profiles:
if self.profileName(profile_id) == name:
self.notifyError(_(
'Profile "{name}" already exists.').format(name=name))
return None
new_id = 1
while True:
ok = True
if str(new_id) in profiles:
ok = False
if ok:
break
new_id = new_id + 1
new_id = str(new_id)
profiles.append(new_id)
self.setStrValue('profiles', ':'.join(profiles))
self.setProfileStrValue('name', name, new_id)
return new_id
[docs]
def removeProfile(self, profile_id=None):
"""
Remove profile and all its keys and values.
Args:
profile_id (str, int): valid profile ID
Returns:
bool: ``True`` if successful
"""
if isinstance(profile_id, int):
profile_id = str(profile_id)
if profile_id is None:
profile_id = self.current_profile_id
profiles = self.profiles()
if len(profiles) <= 1:
self.notifyError(_("The last profile cannot be removed."))
return False
found = False
index = 0
for profile in profiles:
if profile == profile_id:
self.removeKeysStartsWith(self.profileKey('', profile_id))
del profiles[index]
self.setStrValue('profiles', ':'.join(profiles))
found = True
break
index = index + 1
if not found:
return False
if self.current_profile_id == profile_id:
self.current_profile_id = '1'
return True
[docs]
def setProfileName(self, name, profile_id=None):
"""
Change the name of the profile.
Args:
name (str): new profile name
profile_id (str, int): valid profile ID
Returns:
bool: ``True`` if successful.
"""
if isinstance(profile_id, int):
profile_id = str(profile_id)
if profile_id is None:
profile_id = self.current_profile_id
profiles = self.profiles()
for profile in profiles:
if self.profileName(profile) == name:
if profile[0] != profile_id:
self.notifyError(_(
'Profile "{name}" already exists.').format(name=name))
return False
self.setProfileStrValue('name', name, profile_id)
return True
[docs]
def profileKey(self, key, profile_id=None):
"""
Prefix for keys with profile. e.g. 'profile1.key'
Args:
key (str): Key identifier.
profile_id (str, int): Valid profile ID.
Returns:
str: Key with prefix 'profile1.key'
"""
if isinstance(profile_id, int):
profile_id = str(profile_id)
if profile_id is None:
profile_id = self.current_profile_id
return 'profile' + profile_id + '.' + key
[docs]
def removeProfileKey(self, key, profile_id=None):
"""
Remove the key from profile.
Args:
key (str): key name
profile_id (str, int): valid profile ID
"""
self.removeKey(self.profileKey(key, profile_id))
[docs]
def removeProfileKeysStartsWith(self, prefix, profile_id=None):
"""
Remove the keys starting with prefix from profile.
Args:
prefix (str): prefix for keys (key starts with this
string) that should be removed.
profile_id (str, int): valid profile ID
"""
self.removeKeysStartsWith(self.profileKey(prefix, profile_id))
[docs]
def remapProfileKey(self, oldKey, newKey, profileId=None):
"""
Remap profile keys to a new key name.
Args:
oldKey (str): old key name
newKey (str): new key name
profileId (str, int): valid profile ID
"""
self.remapKey(self.profileKey(oldKey, profileId),
self.profileKey(newKey, profileId))
[docs]
def hasProfileKey(self, key, profile_id=None):
"""
``True`` if key is set in profile.
Args:
key (str): string used as key
profile_id (str, int): valid profile ID
Returns:
bool: ``True`` if ``key`` is set.
"""
return self.profileKey(key, profile_id) in self.dict
[docs]
def profileStrValue(self, key, default='', profile_id=None):
"""Return the value of ``key`` related to ``profile_id``.
Returns:
str: The value.
"""
return self.strValue(self.profileKey(key, profile_id), default)
[docs]
def setProfileStrValue(self, key, value, profile_id=None):
self.setStrValue(self.profileKey(key, profile_id), value)
[docs]
def profileIntValue(self, key, default=0, profile_id=None):
return self.intValue(self.profileKey(key, profile_id), default)
[docs]
def setProfileIntValue(self, key, value, profile_id=None):
self.setIntValue(self.profileKey(key, profile_id), value)
[docs]
def profileBoolValue(self, key, default=False, profile_id=None):
return self.boolValue(self.profileKey(key, profile_id), default)
[docs]
def setProfileBoolValue(self, key, value, profile_id=None):
self.setBoolValue(self.profileKey(key, profile_id), value)
[docs]
def profileListValue(self,
key,
type_key='str:value',
default=[],
profile_id=None):
return self.listValue(
self.profileKey(key, profile_id), type_key, default)
[docs]
def setProfileListValue(self, key, type_key, value, profile_id=None):
self.setListValue(self.profileKey(key, profile_id), type_key, value)