view configmix/__init__.py @ 112:c50ad93eb5dc

Implemented a "safe_load()" to load with safe merging
author Franz Glasner <hg@dom66.de>
date Sat, 24 Mar 2018 20:57:42 +0100
parents d51a18e5b0e3
children 21d92ff8cf31
line wrap: on
line source

# -*- coding: utf-8 -*-
"""A library for helping with configuration files.

:Author:    Franz Glasner
:Copyright: (c) 2015–2018, Franz Glasner.
            All rights reserved.
:License:   3-clause BSD License.
            See LICENSE.txt for details.
:ID:        $Header$

"""

from __future__ import division, print_function, absolute_import


__version__ = "0.6.0.dev1"

__revision__ = "$Revision$"


import copy

from .config import Configuration


__all__ = ["load", "safe_load", "Configuration"]


def load(*files):
    """Load the given configuration files, merge them in the given order
    and return the resulting configuration dictionary.

    :param files: the filenames of the configuration files to read and merge
    :returns: the configuration
    :rtype: ~configmix.config.Configuration

    """
    if not files:
        return Configuration()
    else:
        ex = merge(None, _load_cfg_from_file(files[0]))
        for f in files[1:]:
            ex = merge(_load_cfg_from_file(f), ex)
        return Configuration(ex)


def safe_load(*files):
    """Analogous to :func:`load` but do merging with :func:`safe_merge`
    instead of :func:`merge`

    """
    if not files:
        return Configuration()
    else:
        ex = safe_merge(None, _load_cfg_from_file(files[0]))
        for f in files[1:]:
            ex = safe_merge(_load_cfg_from_file(f), ex)
        return Configuration(ex)


def _load_cfg_from_file(filename):
    fnl = filename.lower()
    if fnl.endswith(".yml") or fnl.endswith("yaml"):
        from . import yaml
        with open(filename, "rb") as yf:
            return yaml.safe_load(yf)
    elif fnl.endswith(".py"):
        from . import py
        return py.load(filename)
    elif fnl.endswith(".ini"):
        from . import ini
        return ini.load(filename)
    else:
        raise ValueError("Unknown configuration file type for filename "
                         "%r" % filename)


if 0:
    #
    # From: https://github.com/jet9/python-yconfig/blob/master/yconfig.py
    # License: BSD License
    #
    def dict_merge(a, b):
        """Recursively merges dict's. not just simple a['key'] = b['key'], if
        both a and bhave a key who's value is a dict then dict_merge is called
        on both values and the result stored in the returned dictionary."""

        if not isinstance(b, dict):
            return b
        result = deepcopy(a)
        for k, v in b.iteritems():
            if k in result and isinstance(result[k], dict):
                result[k] = dict_merge(result[k], v)
            else:
                result[k] = deepcopy(v)
        return result


def merge(user, default):
    """Logically merge the configuration in `user` into `default`.

    :param ~configmix.config.Configuration user:
                the new configuration that will be logically merged
                into `default`
    :param ~configmix.config.Configuration default:
                the base configuration where `user` is logically merged into
    :returns: `user` with the necessary amendments from `default`.
              If `user` is ``None`` then `default` is returned.

    .. note:: The configuration in `default` is not changed but the
              configuration given in `user` is changed **inplace**.

    From http://stackoverflow.com/questions/823196/yaml-merge-in-python

    """
    if user is None:
        return default
    if isinstance(user, dict) and isinstance(default, dict):
        for k, v in default.items():
            if k not in user:
                user[k] = v
            else:
                user[k] = _merge(user[k], v)
    return user


def _merge(user, default):
    """Recursion helper for :meth:`merge`

    """
    if isinstance(user, dict) and isinstance(default, dict):
        for k, v in default.items():
            if k not in user:
                user[k] = v
            else:
                user[k] = _merge(user[k], v)
    return user


def safe_merge(user, default):
    """A more safe version of :func:`merge` that makes deep copies of
    the returned container objects.

    No given argument is ever changed inplace. Every object from `default`
    is decoupled from the result -- so changing the `default` configuration
    lates does not yield into a merged configuration later.

    """
    if user is None:
        return copy.deepcopy(default)
    user = copy.deepcopy(user)
    if isinstance(user, dict) and isinstance(default, dict):
        for k, v in default.items():
            if k not in user:
                user[k] = copy.deepcopy(v)
            else:
                user[k] = _safe_merge(user[k], v)
    return user


def _safe_merge(user, default):
    """Recursion helper for :meth:`safe_merge`

    """
    if isinstance(user, dict) and isinstance(default, dict):
        for k, v in default.items():
            if k not in user:
                user[k] = copy.deepcopy(v)
            else:
                user[k] = _safe_merge(user[k], v)
    return user