Source code for reproman.config

# vi: set ft=python sts=4 ts=4 sw=4 et:
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
#
#   See COPYING file distributed along with the reproman package for the
#   copyright and license terms.
#
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Registry-like monster for now simply borrowed from bigmess/pymvpa

TODO: integration with cmdline etc
"""

__docformat__ = 'restructuredtext'

import os.path
from appdirs import AppDirs

from os.path import join as opj

from .support.configparserinc import SafeConfigParserWithIncludes

LOCATIONS_DOC = """
    1. '/etc/reproman/reproman.cfg'
    2. 'reproman/reproman.cfg' in all directories defined by $XDG_CONFIG_DIRS
       (by default: /etc/xdg/)
    3. 'reproman/reproman.cfg' in $XDG_CONFIG_HOME (by default: ~/.config/)
    4. '.reproman/reproman.cfg', relative to the current directory""".lstrip()


[docs]class ConfigManager(SafeConfigParserWithIncludes, object): """Central configuration registry for reproman. The purpose of this class is to collect all configurable settings used by various parts of reproman. It is fairly simple and does only little more than the standard Python ConfigParser. Like ConfigParser it is blind to the data that it stores, i.e. no type checking is performed. Configuration files (INI syntax) in multiple location are parsed when a class instance is created or whenever `Config.reload()` is called later on. Files are read and parsed in the order described by `LOCATIONS_DOC`. Moreover, the constructor takes an optional argument with a list of additional file names to parse afterwards. In addition to configuration files, this class also looks for special environment variables to read settings from. Names of such variables have to start with `REPROMAN_` following by the an optional section name and the variable name itself ('_' as delimiter). If no section name is provided, the variables will be associated with section `general`. Some examples:: REPROMAN_VERBOSE=1 will become:: [general] verbose = 1 However, `REPROMAN_VERBOSE_OUTPUT=stdout` becomes:: [verbose] output = stdout Any length of variable name as allowed, e.g. REPROMAN_SEC1_LONG_NAME=1 becomes:: [sec1] long name = 1 Settings from custom configuration files (specified by the constructor argument) have the highest priority and override settings found in any of the config files read from default locations (which are themselves read in the order stated above -- overwriting earlier configuration settings accordingly). Finally, the content of any `REPROMAN_*` environment variables overrides any settings read from any file. """ dirs = AppDirs("reproman", "reproman.org") # things we want to count on to be available _DEFAULTS = {'general': {'verbose': '1'}} def __init__(self, filenames=None, load_default=True): """Initialization reads settings from config files and env. variables. Parameters ---------- filenames : list of filenames load_default: bool, optional Either to load default files (e.g. system/user-wide etc) """ self._super.__init__() # set critical defaults for sec, params in ConfigManager._DEFAULTS.items(): self.add_section(sec) for key, value in params.items(): self.set(sec, key, value) self.__cfg_filenames = [] self._load_default = load_default # now get the setting # TODO: look into loading/interaction with config being lazy or better more efficient... # could cut on startup time a bit. self.reload(filenames) @property def _super(self): return super(ConfigManager, self) def _get_default_file_candidates(self): # list of filenames to parse (custom plus some standard ones), # they are listed in the order they need to be parsed to # guarantee a sane configuration file cascade homedir = os.path.expanduser('~') # seems to be useless ??? cfg_file_candidates = [ # shipped-with config # opj(os.path.dirname(__file__), 'reproman.cfg'), # system config '/etc/reproman/reproman.cfg'] # XDG system config cfg_file_candidates.append(opj(self.dirs.site_config_dir, 'reproman.cfg')) # XDG user config home_cfg_base_path = self.dirs.user_config_dir if os.path.isabs(home_cfg_base_path): cfg_file_candidates.append(opj(home_cfg_base_path, 'reproman.cfg')) # current dir config cfg_file_candidates.append(opj('.reproman', 'reproman.cfg')) return cfg_file_candidates def _get_file_candidates(self): cfg_file_candidates = self._get_default_file_candidates() if self._load_default else [] # runtime config cfg_file_candidates += self.__cfg_filenames return cfg_file_candidates
[docs] def reload(self, filenames=None): """Re-read settings from all configured locations. """ # store additional config file names if not filenames is None: self.__cfg_filenames = filenames # read local and user-specific config self.read(self._get_file_candidates()) # now look for variables in the environment pref = 'REPROMAN_' for var in [v for v in os.environ.keys() if v.startswith(pref)]: # strip leading 'REPROMAN_' and lower case entries svar = var[len(pref):].lower() # section is next element in name (or 'general' if simple name) if not svar.count('_'): sec = 'general' else: cut = svar.find('_') sec = svar[:cut] svar = svar[cut + 1:].replace('_', ' ') # check if section is already known and add it if not if not self.has_section(sec): self.add_section(sec) # set value self.set(sec, svar, os.environ[var])
[docs] def get(self, section, option, default=None, **kwargs): """Wrapper around SafeConfigParser.get() with a custom default value. This method simply wraps the base class method, but adds a `default` keyword argument. The value of `default` is returned whenever the config parser does not have the requested option and/or section. """ if not self.has_option(section, option): return default try: return self._super.get(section, option, **kwargs) except ValueError as e: # provide somewhat descriptive error raise ValueError( "Failed to obtain value from configuration for %s.%s. " "Original exception was: %s" % (section, option, e))
[docs] def getpath(self, *args, **kwargs): """Wrapper around get to do additional path treatments such as expanduser See documentation for `get` """ val = self.get(*args, **kwargs) return os.path.expanduser(val)
[docs] def getboolean(self, section, option, default=None): """Wrapper around SafeConfigParser.getboolean() with a custom default. This method simply wraps the base class method, but adds a `default` keyword argument. The value of `default` is returned whenever the config parser does not have the requested option and/or section. """ if not self.has_option(section, option): if isinstance(default, bool): return default else: # compatibility layer for py3 version of ConfigParser if hasattr(self, '_boolean_states'): boolean_states = self._boolean_states else: boolean_states = self.BOOLEAN_STATES # there is no BOOLEAN_STATES ??? if default is None or (default.lower() not in boolean_states): raise ValueError('Not a boolean: %s' % default) return boolean_states[default.lower()] return self._super.getboolean(section, option)
##REF: Name was automagically refactored
[docs] def get_as_dtype(self, section, option, dtype, default=None): """Convenience method to query options with a custom default and type This method simply wraps the base class method, but adds a `default` keyword argument. The value of `default` is returned whenever the config parser does not have the requested option and/or section. In addition, the returned value is converted into the specified `dtype`. """ if not self.has_option(section, option): return default try: return self._super._get(section, dtype, option) except ValueError as e: # provide somewhat descriptive error raise ValueError( "Failed to obtain value from configuration for %s.%s. " "Original exception was: %s" % (section, option, e))