Source code for staticconf.loader

"""
Load configuration values from different file formats and python structures.
Nested dictionaries are flattened using dotted notation.

These flattened keys and values are merged into a
:class:`staticconf.config.ConfigNamespace`.

Examples
--------

Basic example:

.. code-block:: python

    staticconf.YamlConfiguration('config.yaml')


Multiple loaders can be used to override values from previous loaders.

.. code-block:: python

    import staticconf

    # Start by loading some values from a defaults file
    staticconf.YamlConfiguration('defaults.yaml')

    # Override with some user specified options
    staticconf.YamlConfiguration('user.yaml', optional=True)

    # Further override with some command line options
    staticconf.ListConfiguration(opts.config_values)


For configuration reloading see :class:`staticconf.config.ConfigFacade`.


Arguments
---------

Configuration loaders accept the following kwargs:

error_on_unknown
    raises a :class:`staticconf.errors.ConfigurationError` if there are keys
    in the config that have not been defined by a getter or a schema.

optional
    if True only warns on failure to load configuration (Default False)

namespace
    load the configuration values into a namespace. Defaults to the
    `DEFAULT` namespace.

flatten
    flatten nested structures into a mapping with depth of 1 (Default True)


.. versionadded:: 0.7.0
    `flatten` was added as a kwarg to all loaders


Custom Loader
-------------

You can create your own loaders for other formats by using
:func:`build_loader()`.  It takes a single argument, a function, which
can accept any arguments, but must return a dictionary of
configuration values.


.. code-block:: python

    from staticconf import loader

    def load_from_db(table_name, conn):
        ...
        return dict((row.field, row.value) for row in cursor.fetchall())

    DBConfiguration = loader.build_loader(load_from_db)

    # Now lets use it
    DBConfiguration('config_table', conn, namespace='special')


"""
import logging
import os
import re

import six
from six.moves import (
    configparser,
    filter,
    reload_module,
)

from staticconf import config, errors

__all__ = [
    'YamlConfiguration',
    'JSONConfiguration',
    'ListConfiguration',
    'DictConfiguration',
    'AutoConfiguration',
    'PythonConfiguration',
    'INIConfiguration',
    'XMLConfiguration',
    'PropertiesConfiguration',
    'CompositeConfiguration',
    'ObjectConfiguration',
]


log = logging.getLogger(__name__)


def flatten_dict(config_data):
    for key, value in six.iteritems(config_data):
        if hasattr(value, 'items') or hasattr(value, 'iteritems'):
            for k, v in flatten_dict(value):
                yield '%s.%s' % (key, k), v
            continue

        yield key, value


def load_config_data(loader_func, *args, **kwargs):
    optional = kwargs.pop('optional', False)
    try:
        return loader_func(*args, **kwargs)
    except Exception as e:
        log.info("Optional configuration failed: %s" % e)
        if not optional:
            raise
        return {}


def build_loader(loader_func):
    def loader(*args, **kwargs):
        err_on_unknown      = kwargs.pop('error_on_unknown', False)
        log_keys_only       = kwargs.pop('log_keys_only', False)
        err_on_dupe         = kwargs.pop('error_on_duplicate', False)
        flatten             = kwargs.pop('flatten', True)
        name                = kwargs.pop('namespace', config.DEFAULT)

        config_data = load_config_data(loader_func, *args, **kwargs)
        if flatten:
            config_data = dict(flatten_dict(config_data))
        namespace   = config.get_namespace(name)
        namespace.apply_config_data(
            config_data,
            err_on_unknown,
            err_on_dupe,
            log_keys_only=log_keys_only,
        )
        return config_data

    return loader


def yaml_loader(filename):
    import yaml
    try:
        from yaml import CLoader as Loader
    except ImportError:
        from yaml import Loader

    with open(filename) as fh:
        return yaml.load(fh, Loader=Loader) or {}


def json_loader(filename):
    try:
        import simplejson as json
        assert json
    except ImportError:
        import json
    with open(filename) as fh:
        return json.load(fh)


def list_loader(seq):
    return dict(pair.split('=', 1) for pair in seq)


def auto_loader(base_dir='.', auto_configurations=None):
    auto_configurations = auto_configurations or [
        (yaml_loader,       'config.yaml'),
        (json_loader,       'config.json'),
        (ini_file_loader,   'config.ini'),
        (xml_loader,        'config.xml'),
        (properties_loader, 'config.properties')
    ]

    for config_loader, config_arg in auto_configurations:
        path = os.path.join(base_dir, config_arg)
        if os.path.isfile(path):
            return config_loader(path)
    msg = "Failed to auto-load configuration. No configuration files found."
    raise errors.ConfigurationError(msg)


def python_loader(module_name):
    module = __import__(module_name, fromlist=['*'])
    reload_module(module)
    return object_loader(module)


def object_loader(obj):
    return dict((name, getattr(obj, name))
                for name in dir(obj) if not name.startswith('_'))


def ini_file_loader(filename):
    parser = configparser.SafeConfigParser()
    parser.read([filename])
    config_dict = {}

    for section in parser.sections():
        for key, value in parser.items(section, True):
            config_dict['%s.%s' % (section, key)] = value

    return config_dict


def xml_loader(filename, safe=False):
    from xml.etree import ElementTree

    def build_from_element(element):
        items = dict(element.items())
        child_items = dict(
            (child.tag, build_from_element(child))
            for child in element)

        config.has_duplicate_keys(child_items, items, safe)
        items.update(child_items)
        if element.text:
            if 'value' in items and safe:
                msg = "%s has tag with child or attribute named value."
                raise errors.ConfigurationError(msg % filename)
            items['value'] = element.text
        return items

    tree = ElementTree.parse(filename)
    return build_from_element(tree.getroot())


def properties_loader(filename):
    split_pattern = re.compile(r'[=:]')

    def parse_line(line):
        line = line.strip()
        if not line or line.startswith('#'):
            return

        try:
            key, value = split_pattern.split(line, 1)
        except ValueError:
            msg = "Invalid properties line: %s" % line
            raise errors.ConfigurationError(msg)
        return key.strip(), value.strip()

    with open(filename) as fh:
        return dict(filter(None, (parse_line(line) for line in fh)))


[docs]class CompositeConfiguration(object): """Store a list of configuration loaders and their params, so they can be reloaded in the correct order. """ def __init__(self, loaders=None): self.loaders = loaders or []
[docs] def append(self, loader): self.loaders.append(loader)
[docs] def load(self): output = {} for loader_with_args in self.loaders: output.update(loader_with_args[0](*loader_with_args[1:])) return output
def __call__(self, *args): return self.load()
YamlConfiguration = build_loader(yaml_loader) """Load configuration from a yaml file. :param filename: path to a yaml file """ JSONConfiguration = build_loader(json_loader) """Load configuration from a json file. :param filename: path to a json file """ ListConfiguration = build_loader(list_loader) """Load configuration from a list of strings in the form `key=value`. :param seq: a sequence of strings """ DictConfiguration = build_loader(lambda d: d) """Load configuration from a :class:`dict`. :param dict: a dictionary """ ObjectConfiguration = build_loader(object_loader) """Load configuration from any object. Attributes are keys and the attribute value are values. :param object: an object """ AutoConfiguration = build_loader(auto_loader) """ .. deprecated:: v0.7.0 Do not use. It will be removed in future versions. """ PythonConfiguration = build_loader(python_loader) """Load configuration from a python module. :param module: python path to a module as you would pass it to :func:`__import__` """ INIConfiguration = build_loader(ini_file_loader) """Load configuration from a .ini file :param filename: path to the ini file """ XMLConfiguration = build_loader(xml_loader) """Load configuration from an XML file. :param filename: path to the XML file """ PropertiesConfiguration = build_loader(properties_loader) """Load configuration from a properties file :param filename: path to the properties file """