Source code for spreads.config

# -*- coding: utf-8 -*-

# Copyright (C) 2014 Johannes Baiter <johannes.baiter@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Configuration entities.
"""

from __future__ import unicode_literals

import copy
import logging

import spreads.vendor.confit as confit
from pathlib import Path

import spreads.util as util


[docs]class OptionTemplate(object): """ Definition of a configuration option. :attr value: The default value for the option or a list of available options if :py:attr`selectable` is True :type value: object (or list/tuple when :py:attr:`selectable` is True) :attr docstring: A string explaining the configuration option :type docstring: unicode :attr selectable: Make the `OptionTemplate` a selectable, i.e. value contains a list or tuple of acceptable values for this option, with the first member being the default selection. :type selectable: bool :attr advanced: Whether the option is an advanced option :type advanced: bool :attr depends: Make option dependant of some other setting (if passed a dict) or another plugin (if passed a string) :type depends: dict/str """
[docs] def __init__(self, value, docstring=None, selectable=False, advanced=False, depends=None): self.value = value self.docstring = docstring self.selectable = selectable self.advanced = advanced self.depends = depends
def __repr__(self): return ("OptionTemplate(value={0}, docstring={1}, selectable={2}" " advanced={3}, depends={4})" .format(repr(self.value), repr(self.docstring), repr(self.selectable), repr(self.advanced), repr(self.depends)))
# Configuration templates for the core CORE_OPTIONS = { 'verbose': OptionTemplate(value=False, docstring="Enable verbose output"), 'logfile': OptionTemplate( value=unicode(Path(util.get_data_dir())/'spreads.log'), docstring="Path to logfile"), 'loglevel': OptionTemplate(value=['info', 'critical', 'error', 'warning', 'debug'], docstring="Logging level for logfile", selectable=True), 'capture_keys': OptionTemplate(value=[" ", "b"], docstring="Keys to trigger capture", selectable=False), 'convert_old': OptionTemplate( value=False, docstring=("Convert workflows from older spreads version to the new " "directory layout."), advanced=True) }
[docs]class Configuration(object): """ Entity managing configuration state. Uses :py:class:`confit.Configuration` underneath the hood and follows its 'overlay'-principle. Proxies :py:meth:`__getitem__` and :py:meth:`__setitem__` from it, so it can be used as a dict-like type. """
[docs] def __init__(self, appname='spreads'): """ Create new instance and load default and current configuration. :param appname: Application name, configuration will be loaded from this name's default configuration directory """ self._config = confit.Configuration(appname, __name__) self._config.read() if 'plugins' not in self._config.keys(): self['plugins'] = [] self.load_templates() self.load_defaults(overwrite=False)
# ----------------------------------------- # # Proxied methods from confit.Configuration # def __getitem__(self, key): """ See :py:meth:`confit.ConfigView.__getitem__` """ return self._config[key] def __setitem__(self, key, value): """ See :py:meth:`confit.ConfigView.__setitem__` """ self._config[key] = value
[docs] def keys(self): """ See :py:meth:`confit.ConfigView.keys` """ return self._config.keys()
[docs] def dump(self, filename=None, full=True, sections=None): """ See :py:meth:`confit.Configuration.dump` """ return self._config.dump(unicode(filename), full, sections)
[docs] def flatten(self): """ See :py:meth:`confit.Configuration.flatten` """ return self._config.flatten()
# ----------------------------------------- #
[docs] def load_templates(self): """ Get all available configuration templates from the activated plugins. :returns: Mapping from plugin name to template mappings. :rtype: dict unicode -> (dict unicode -> :py:class:`OptionTemplate`) """ import spreads.plugin self.templates = {} self.templates['core'] = CORE_OPTIONS if 'driver' in self.keys(): driver_name = self["driver"].get() self.templates['device'] = ( spreads.plugin.get_driver(driver_name) .configuration_template()) plugins = spreads.plugin.get_plugins(*self["plugins"].get()) for name, plugin in plugins.iteritems(): tmpl = plugin.configuration_template() if tmpl: self.templates[name] = tmpl return self.templates
@property def cfg_path(self): """ Path to YAML file of the user-specific configuration. :returns: Path :rtype: :py:class:`pathlib.Path` """ return Path(self._config.config_dir()) / confit.CONFIG_FILENAME
[docs] def with_overlay(self, overlay): """ Get a new configuration that overlays the provided configuration over the present configuration. :param overlay: The configuration to be overlaid :type overlay: :py:class:`confit.ConfigSource` or dict :return: A new, merged configuration :rtype: :py:class:`confit.Configuration` """ new_config = copy.deepcopy(self._config) new_config.set(overlay) return new_config
[docs] def as_view(self): """ Return the `Configuration` as a :py:class:`confit.ConfigView` instance. """ return self._config
[docs] def load_defaults(self, overwrite=True): """ Load default settings from option templates. :param overwrite: Whether to overwrite already existing values """ for section, template in self.templates.iteritems(): self.set_from_template(section, template, overwrite)
[docs] def set_from_template(self, section, template, overwrite=True): """ Set default options from templates. :param section: Target section for settings :type section: unicode :type template: :py:class:`OptionTemplate` :param overwrite: Whether to overwrite already existing values """ old_settings = self[section].flatten() settings = copy.deepcopy(old_settings) for key, option in template.iteritems(): logging.info("Adding setting {0} from {1}" .format(key, section)) if not overwrite and key in old_settings: continue if option.selectable: settings[key] = option.value[0] else: settings[key] = option.value self[section].set(settings)
[docs] def set_from_args(self, args): """ Apply settings from parsed command-line arguments. :param args: Parsed command-line arguments :type args: :py:class:`argparse.Namespace` """ for argkey, value in args.__dict__.iteritems(): skip = (value is None or argkey == 'subcommand' or argkey.startswith('_')) if skip: continue if '.' in argkey: section, key = argkey.split('.') self[section][key] = value else: self[argkey] = value