09a875db11
Making use of the fact that GrampsLocale now knows what encoding to use, and noting that filesystems don't use more than one encoding to write filenames in directories. Also specify the encoding on some more files svn: r21148
551 lines
21 KiB
Python
551 lines
21 KiB
Python
#
|
|
# Gramps - a GTK+/GNOME based genealogy program
|
|
#
|
|
# Copyright (C) 2005-2007 Donald N. Allingham
|
|
# Copyright (C) 2008-2009 Gary Burton
|
|
# Copyright (C) 2009 Doug Blank <doug.blank@gmail.com>
|
|
#
|
|
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
|
|
# $Id$
|
|
|
|
"""
|
|
This package implements access to GRAMPS configuration.
|
|
"""
|
|
|
|
#---------------------------------------------------------------
|
|
#
|
|
# System imports
|
|
#
|
|
#---------------------------------------------------------------
|
|
from __future__ import print_function, unicode_literals
|
|
import os
|
|
import sys
|
|
import time
|
|
if sys.version_info[0] < 3:
|
|
import ConfigParser as configparser
|
|
else:
|
|
import configparser
|
|
import errno
|
|
import copy
|
|
import logging
|
|
import io
|
|
|
|
from ..constfunc import STRTYPE
|
|
from ..const import GRAMPS_LOCALE as glocale
|
|
_ = glocale.get_translation().gettext
|
|
def safe_eval(exp):
|
|
# restrict eval to empty environment
|
|
return eval(exp, {})
|
|
|
|
##try:
|
|
## from ast import literal_eval as safe_eval
|
|
## # this leaks memory !!
|
|
##except:
|
|
## # PYTHON2.5 COMPATIBILITY: no ast present
|
|
## # not as safe as literal_eval, but works for python2.5:
|
|
## def safe_eval(exp):
|
|
## # restrict eval to empty environment
|
|
## return eval(exp, {})
|
|
|
|
#---------------------------------------------------------------
|
|
#
|
|
# Classes
|
|
#
|
|
#---------------------------------------------------------------
|
|
class ConfigManager(object):
|
|
"""
|
|
Class to construct the singleton CONFIGMAN where all
|
|
settings are stored.
|
|
"""
|
|
PLUGINS = {}
|
|
|
|
def __init__(self, filename=None, plugins=None):
|
|
"""
|
|
Configure manager constructor takes an optional filename, and
|
|
plugin path.
|
|
|
|
The data dictionary stores the settings:
|
|
|
|
self.data[section][setting] = value
|
|
|
|
The value has a type that matches the default. It is an error
|
|
to attempt to set the setting to a different type. To change
|
|
the type, you must re-register the setting, and re-set the
|
|
value. Values can be any simple type in Python (except,
|
|
currently longs, which are saved as ints to avoid type
|
|
errors). This includes: str, int, list, tuple, dict, float,
|
|
etc. Of course, composite types must themselves be composed of
|
|
simple types.
|
|
|
|
The default values are given in Python code and stored here
|
|
on start-up:
|
|
|
|
self.default[section][setting] = default_value
|
|
|
|
Callbacks are stored as callables here:
|
|
|
|
self.callbacks[section][setting] = (id, func)
|
|
|
|
The default filename (usually the one you are reading from)
|
|
is stored as self.filename. However, you can save to another
|
|
filename using self.save(otherfilename).
|
|
|
|
filename (if given) is a fullpath.
|
|
plugins (if given) is a relative path to filename.
|
|
|
|
"""
|
|
self._cb_id = 0 # callback id counter
|
|
self.config_path, config_filename = \
|
|
os.path.split(os.path.abspath(filename))
|
|
self.filename = filename # fullpath and filename, or None
|
|
self.plugins = plugins # relative directory name, or None
|
|
self.callbacks = {}
|
|
self.default = {}
|
|
self.data = {}
|
|
self.reset()
|
|
|
|
def register_manager(self, name, override="", use_plugins_path=True,
|
|
use_config_path=False):
|
|
"""
|
|
Register a plugin manager.
|
|
|
|
name is used as the key of the config manager singleton. It is
|
|
also be used as the base filename (unless an override is given,
|
|
or use_config_path or use_plugins_path is True).
|
|
|
|
override is either:
|
|
- a full path+filename ending in .ini
|
|
- a filename ending in .ini
|
|
- a dir path to put ini file into
|
|
- a full path+filename to get dir to put ini file into
|
|
- a ConfigManager instance
|
|
|
|
If use_config_path is True, use this ConfigManager's path as
|
|
the new manager's path, ignoring any path given in override.
|
|
|
|
Examples:
|
|
>>> config.register_manager("Simple", use_plugins_path=False)
|
|
# will use the calling programs directory, and "Simple.ini"
|
|
>>> config.register_manager("Simple", __file__,
|
|
use_plugins_path=False)
|
|
# will use the __file__'s directory, and "Simple.ini"
|
|
>>> config.register_manager("Simple", "c:\\temp",
|
|
use_plugins_path=False)
|
|
# will use the given directory, "c:\\temp\\Simple.ini"
|
|
>>> config.register_manager("Simple", use_config_path=True)
|
|
# will use config's path: ~/.gramps/gramps32/Simple.ini
|
|
>>> config.register_manager("Simple", "Other.ini")
|
|
# will use config + plugins path: ~/.gramps/gramps32/plugins/Other.ini
|
|
>>> config.register_manager("Simple", "/tmp/Other.ini",
|
|
use_plugins_path=False)
|
|
# will use /tmp/Other.ini
|
|
"""
|
|
if isinstance(override, STRTYPE): # directory or filename
|
|
if override:
|
|
path, ininame = os.path.split(os.path.abspath(override))
|
|
else:
|
|
path, ininame = os.path.split(sys._getframe(1).f_code.co_filename)
|
|
if not ininame.endswith(".ini"):
|
|
ininame = "%s.ini" % name
|
|
if use_config_path:
|
|
path = self.config_path
|
|
elif use_plugins_path:
|
|
path = os.path.join(self.config_path, self.plugins)
|
|
filename = os.path.join(path, ininame)
|
|
plugin = ConfigManager(filename)
|
|
elif isinstance(override, ConfigManager):
|
|
plugin = override
|
|
else:
|
|
raise AttributeError("plugin needs to be a file or ConfigManager")
|
|
ConfigManager.PLUGINS[name] = plugin
|
|
return ConfigManager.PLUGINS[name]
|
|
|
|
def get_manager(self, name):
|
|
if name in ConfigManager.PLUGINS:
|
|
return ConfigManager.PLUGINS[name]
|
|
else:
|
|
raise AttributeError("config '%s': does not exist"% name)
|
|
|
|
def has_manager(self, name):
|
|
return name in ConfigManager.PLUGINS
|
|
|
|
def init(self):
|
|
"""
|
|
Either loads from an existing ini file, or saves to it.
|
|
"""
|
|
if self.filename:
|
|
if os.path.exists(self.filename):
|
|
self.load()
|
|
else:
|
|
self.save()
|
|
|
|
def __getitem__(self, item):
|
|
"""
|
|
For item access, such as config["interface.dont-ask"]
|
|
"""
|
|
return self.get(item)
|
|
|
|
def __setitem__(self, item, value):
|
|
"""
|
|
For item assignment, such as config["interface.dont-ask"] = True
|
|
"""
|
|
self.set(item, value)
|
|
|
|
def reset(self, key=None):
|
|
"""
|
|
Resets one, a section, or all settings values to their defaults.
|
|
This does not disconnect callbacks.
|
|
"""
|
|
if key is None:
|
|
section = None
|
|
setting = None
|
|
elif "." in key:
|
|
section, setting = key.split(".", 1)
|
|
else: # key is not None and doesn't have a "."
|
|
section = key
|
|
setting = None
|
|
# Now, do the reset on the right parts:
|
|
if section is None:
|
|
self.data = {}
|
|
for section in self.default:
|
|
self.data[section] = {}
|
|
for setting in self.default[section]:
|
|
self.data[section][setting] = \
|
|
copy.deepcopy(self.default[section][setting])
|
|
elif setting is None:
|
|
self.data[section] = {}
|
|
for setting in self.default[section]:
|
|
self.data[section][setting] = \
|
|
copy.deepcopy(self.default[section][setting])
|
|
else:
|
|
self.data[section][setting] = \
|
|
copy.deepcopy(self.default[section][setting])
|
|
# Callbacks are still connected
|
|
|
|
def get_sections(self):
|
|
"""
|
|
Return all section names.
|
|
"""
|
|
return list(self.data.keys())
|
|
|
|
def get_section_settings(self, section):
|
|
"""
|
|
Return all section setting names.
|
|
"""
|
|
return list(self.data[section].keys())
|
|
|
|
def load(self, filename=None, oldstyle=False):
|
|
"""
|
|
Loads an .ini into self.data.
|
|
"""
|
|
if filename is None:
|
|
filename = self.filename
|
|
if filename and os.path.exists(filename):
|
|
parser = configparser.RawConfigParser()
|
|
try: # see bugs 5356, 5490, 5591, 5651, 5718, etc.
|
|
parser.read(filename)
|
|
except:
|
|
msg1 = _("WARNING: could not parse file, recreating it:\n%s")
|
|
print(msg1 % filename, file=sys.stderr)
|
|
return
|
|
for sec in parser.sections():
|
|
name = sec.lower()
|
|
if name not in self.data:
|
|
# Add the setting from file
|
|
# These might be old settings, or third-party settings
|
|
self.data[name] = {}
|
|
for opt in parser.options(sec):
|
|
raw_value = parser.get(sec, opt).strip()
|
|
if sys.version_info[0] >= 3:
|
|
if raw_value[:2] == "u'":
|
|
raw_value = raw_value[1:]
|
|
elif raw_value.startswith('['):
|
|
raw_value = raw_value.replace(", u'", ", '")
|
|
raw_value = raw_value.replace("[u'", "['")
|
|
setting = opt.lower()
|
|
if oldstyle:
|
|
####################### Upgrade from oldstyle < 3.2
|
|
# Oldstyle didn't mark setting type, but had it
|
|
# set in preferences. New style gets it from evaling
|
|
# the setting's value
|
|
#######################
|
|
# if we know this setting, convert type
|
|
key = "%s.%s" % (name, setting)
|
|
if self.has_default(key):
|
|
vtype = type(self.get_default(key))
|
|
if vtype == bool:
|
|
value = raw_value in ["1", "True"]
|
|
elif vtype == list:
|
|
logging.warning("WARNING: ignoring old key '%s'" % key)
|
|
continue # there were no lists in oldstyle
|
|
else:
|
|
value = vtype(raw_value)
|
|
else:
|
|
# else, ignore it
|
|
logging.warning("WARNING: ignoring old key '%s'" % key)
|
|
continue # with next setting
|
|
####################### End upgrade code
|
|
else:
|
|
value = safe_eval(raw_value)
|
|
####################### Now, let's test and set:
|
|
if (name in self.default and
|
|
setting in self.default[name]):
|
|
if isinstance(self.default[name][setting], bool):
|
|
#make sure 0 and 1 are False and True
|
|
if value == 0:
|
|
value = False
|
|
elif value == 1:
|
|
value = True
|
|
if self.check_type(self.default[name][setting], value):
|
|
self.data[name][setting] = value
|
|
else:
|
|
logging.warning("WARNING: ignoring key with wrong type "
|
|
"'%s.%s' %s needed instead of %s" %
|
|
(name, setting,
|
|
type(self.data[name][setting]),
|
|
type(value)))
|
|
else:
|
|
# this could be a third-party setting; add it:
|
|
self.data[name][setting] = value
|
|
|
|
def save(self, filename = None):
|
|
"""
|
|
Saves the current section/settings to an .ini file. Optional filename
|
|
will override the default filename to save to, if given.
|
|
"""
|
|
if filename is None:
|
|
filename = self.filename
|
|
if filename:
|
|
try:
|
|
head = os.path.split( filename )[0]
|
|
os.makedirs( head )
|
|
except OSError as exp:
|
|
if exp.errno != errno.EEXIST:
|
|
raise
|
|
key_file = io.open(filename, "w", encoding = "utf-8")
|
|
key_file.write(";; Gramps key file\n")
|
|
key_file.write((";; Automatically created at %s" %
|
|
time.strftime("%Y/%m/%d %H:%M:%S")) + "\n\n")
|
|
sections = sorted(self.data)
|
|
for section in sections:
|
|
key_file.write(("[%s]\n") % section)
|
|
keys = sorted(self.data[section])
|
|
for key in keys:
|
|
value = self.data[section][key]
|
|
# If it has a default:
|
|
if self.has_default("%s.%s" % (section, key)):
|
|
if value == self.get_default("%s.%s" % (section, key)):
|
|
default = ";;"
|
|
else:
|
|
default = ""
|
|
if isinstance(value, int):
|
|
value = int(value)
|
|
key_file.write(("%s%s=%s\n")% (default,
|
|
key,
|
|
repr(value)))
|
|
key_file.write("\n")
|
|
key_file.close()
|
|
# else, no filename given; nothing to save so do nothing quietly
|
|
|
|
def get(self, key):
|
|
"""
|
|
Get the setting's value. raise an error if an invalid section.setting.
|
|
Key is a sting in the "section.setting" format.
|
|
"""
|
|
if "." in key:
|
|
section, setting = key.split(".", 1)
|
|
else:
|
|
raise AttributeError("Invalid config section.setting name: '%s'" %
|
|
key)
|
|
if section not in self.data:
|
|
raise AttributeError("No such config section name: '%s'" % section)
|
|
if setting not in self.data[section]:
|
|
raise AttributeError("No such config setting name: '%s.%s'" %
|
|
(section, setting))
|
|
return self.data[section][setting]
|
|
|
|
def is_set(self, key):
|
|
"""
|
|
Does the setting exist? Returns True if does, False otherwise.
|
|
Key is a sting in the "section.setting" format.
|
|
"""
|
|
if "." in key:
|
|
section, setting = key.split(".", 1)
|
|
else:
|
|
return False
|
|
if section not in self.data:
|
|
return False
|
|
if setting not in self.data[section]:
|
|
return False
|
|
return True
|
|
|
|
def has_default(self, key):
|
|
"""
|
|
Does the setting have a default value? Returns True if it does,
|
|
False otherwise. Key is a sting in the "section.setting" format.
|
|
"""
|
|
if "." in key:
|
|
section, setting = key.split(".", 1)
|
|
else:
|
|
return False
|
|
if section not in self.default:
|
|
return False
|
|
if setting not in self.default[section]:
|
|
return False
|
|
return True
|
|
|
|
def get_default(self, key):
|
|
"""
|
|
Get the setting's default value. Raises an error if invalid key is
|
|
give. Key is a sting in the "section.setting" format.
|
|
"""
|
|
if "." in key:
|
|
section, setting = key.split(".", 1)
|
|
else:
|
|
raise AttributeError("Invalid config section.setting name: '%s'" %
|
|
key)
|
|
if section not in self.default:
|
|
raise AttributeError("No such config section name: '%s'" % section)
|
|
if setting not in self.default[section]:
|
|
raise AttributeError("No such config setting name: '%s.%s'" %
|
|
(section, setting))
|
|
return self.default[section][setting]
|
|
|
|
def register(self, key, default):
|
|
"""
|
|
Register a section.setting, and set the default.
|
|
Will overwrite any previously set default, and set setting if not one.
|
|
The default value deterimines the type of the setting.
|
|
"""
|
|
if "." in key:
|
|
section, setting = key.split(".", 1)
|
|
else:
|
|
raise AttributeError("Invalid config section.setting name: '%s'" %
|
|
key)
|
|
if section not in self.data:
|
|
self.data[section] = {}
|
|
if section not in self.default:
|
|
self.default[section] = {}
|
|
if section not in self.callbacks:
|
|
self.callbacks[section] = {}
|
|
if setting not in self.callbacks[section]:
|
|
self.callbacks[section][setting] = []
|
|
# Add the default value to settings, if not exist:
|
|
if setting not in self.data[section]:
|
|
self.data[section][setting] = default
|
|
# Set the default, regardless:
|
|
self.default[section][setting] = copy.deepcopy(default)
|
|
|
|
def connect(self, key, func):
|
|
"""
|
|
Connect a callback func that gets called when key is changed.
|
|
"""
|
|
if "." in key:
|
|
section, setting = key.split(".", 1)
|
|
else:
|
|
raise AttributeError("Invalid config section.setting name: '%s'" %
|
|
key)
|
|
if section not in self.data:
|
|
raise AttributeError("No such config section name: '%s'" % section)
|
|
if setting not in self.data[section]:
|
|
raise AttributeError("No such config setting name: '%s.%s'" %
|
|
(section, setting))
|
|
self._cb_id += 1
|
|
self.callbacks[section][setting].append((self._cb_id, func))
|
|
return self._cb_id
|
|
|
|
def disconnect(self, callback_id):
|
|
"""
|
|
Removes a callback given its callback ID. The ID is generated and
|
|
returned when the function is connected to the key (section.setting).
|
|
"""
|
|
for section in self.callbacks:
|
|
for setting in self.callbacks[section]:
|
|
for (cbid, func) in self.callbacks[section][setting]:
|
|
if callback_id == cbid:
|
|
self.callbacks[section][setting].remove((cbid, func))
|
|
|
|
def emit(self, key):
|
|
"""
|
|
Emits the signal "key" which will call the callbacks associated
|
|
with that setting.
|
|
"""
|
|
if "." in key:
|
|
section, setting = key.split(".", 1)
|
|
else:
|
|
raise AttributeError("Invalid config section.setting name: '%s'" %
|
|
key)
|
|
if section not in self.callbacks:
|
|
raise AttributeError("No such config section name: '%s'" % section)
|
|
if setting not in self.callbacks[section]:
|
|
raise AttributeError("No such config setting name: '%s.%s'" %
|
|
(section, setting))
|
|
for (cbid, func) in self.callbacks[section][setting]:
|
|
func(self, 0, str(self.data[section][setting]), None)
|
|
|
|
def set(self, key, value):
|
|
"""
|
|
Set the setting's value. There are only two ways to get into
|
|
the data dictionary: via the load() method that reads a file,
|
|
or from this method.
|
|
"""
|
|
if "." in key:
|
|
section, setting = key.split(".", 1)
|
|
else:
|
|
raise AttributeError("Invalid config section.setting name: '%s'" %
|
|
key)
|
|
if section not in self.data:
|
|
raise AttributeError("No such config section name: '%s'" % section)
|
|
if setting not in self.data[section]:
|
|
raise AttributeError("No such config setting name: '%s.%s'" %
|
|
(section, setting))
|
|
# Check value to see if right type:
|
|
if self.has_default(key):
|
|
if not self.check_type(self.get_default(key), value):
|
|
raise AttributeError("attempting to set '%s' to wrong type "
|
|
"'%s'; should be '%s'" %
|
|
(key, type(value),
|
|
type(self.get_default(key))))
|
|
if (setting in self.data[section] and
|
|
self.data[section][setting] == value):
|
|
# Do nothing if existed and is the same
|
|
pass
|
|
else:
|
|
# Set the value:
|
|
self.data[section][setting] = value
|
|
# Only call callback if the value changed!
|
|
if (section in self.callbacks and
|
|
setting in self.callbacks[section]):
|
|
self.emit(key)
|
|
|
|
def check_type(self, value1, value2):
|
|
"""
|
|
Check if value1 and value2 are different types.
|
|
"""
|
|
type1 = type(value1)
|
|
type2 = type(value2)
|
|
if type1 == type2:
|
|
return True
|
|
elif (isinstance(value1, STRTYPE) and
|
|
isinstance(value2, STRTYPE)):
|
|
return True
|
|
elif (type1 in [int, float] and
|
|
type2 in [int, float]):
|
|
return True
|
|
else:
|
|
return False
|