Source code for gramps.gen.utils.configmanager

#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

"""
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.translation.gettext

[docs]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 # #---------------------------------------------------------------
[docs]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). :param filename: (if given) is a fullpath. :param 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()
[docs] def register_manager(self, name, override="", use_plugins_path=True, use_config_path=False): """ Register a plugin manager. :param 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). :param override: is used to override the default location of the .ini file. :param use_config_path: if True, use this ConfigManager's path as the new manager's path, ignoring any path given in override. 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 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]
[docs] def get_manager(self, name): if name in ConfigManager.PLUGINS: return ConfigManager.PLUGINS[name] else: raise AttributeError("config '%s': does not exist"% name)
[docs] def has_manager(self, name): return name in ConfigManager.PLUGINS
[docs] 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)
[docs] 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
[docs] def get_sections(self): """ Return all section names. """ return list(self.data.keys())
[docs] def get_section_settings(self, section): """ Return all section setting names. """ return list(self.data[section].keys())
[docs] 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. if sys.version_info[0] >= 3 : parser.read(filename, encoding='utf8') else: parser.readfp(io.open(filename, encoding="utf-8")) except Exception as err: msg1 = _("WARNING: could not parse file %s because %s, recreating it:\n") logging.warn(msg1 % (filename, str(err))) 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
[docs] 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 try: key_file = io.open(filename, "w", encoding="utf-8") except IOError as err: logging.warn("Failed to open %s because %s", filename, str(err)) return; 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
[docs] 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]
[docs] 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
[docs] 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
[docs] 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]
[docs] 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)
[docs] 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
[docs] 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))
[docs] 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)
[docs] def set(self, key, value): """ Set the setting's value. There are only two ways to get into the data dictionary: via the :meth:`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)
[docs] 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