view configmix/config.py @ 284:4aaf74858d07

Some links to AWS docu into the aws moduleSome links to AWS docu into the aws moduleSome links to AWS docu into the aws moduleSome links to AWS docu into the aws moduleSome links to AWS docu into the aws moduleSome links to AWS docu into the aws moduleSome links to AWS docu into the aws moduleSome links to AWS docu into the aws module
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 07 Dec 2020 01:59:11 +0100
parents 2a8dcab2de8c
children eed16a1ec8f3
line wrap: on
line source

# -*- coding: utf-8 -*-
# :-
# :Copyright: (c) 2015-2020, Franz Glasner. All rights reserved.
# :License:   3-clause BSD. See LICENSE.txt for details.
# :-
"""The unified configuration dictionary with attribute support or
variable substitution.

"""

from __future__ import division, absolute_import, print_function


__all__ = ["Configuration"]


import warnings
try:
    from collections import OrderedDict as ConfigurationBase
except ImportError:
    try:
        from ordereddict import OrderedDict as ConfigurationBase
    except ImportError:
        ConfigurationBase = dict

from .variables import lookup_varns, lookup_filter
from .compat import u


_MARKER = object()


class _AttributeDict(ConfigurationBase):

    def __getattr__(self, name):
        try:
            v = self[name]
        except KeyError:
            raise AttributeError("%s has no attribute %r" % (type(self), name))
        else:
            # Wrap a dict into another dict with attribute access support
            if isinstance(v, dict):
                return _AttributeDict(v)
            else:
                return v


class Configuration(_AttributeDict):

    """The configuration dictionary with attribute support or
    variable substitution.

    .. note:: When retriving by attribute names variables will *not*
              substituted.

    """

    def getvar(self, varname, default=_MARKER):
        """Get a variable of the form ``[ns:][[key1.]key2.]name`` - including
        variables from other namespaces.

        No variable interpolation is done and no filters are applied.

        """
        varns, varname = self._split_ns(varname)
        try:
            if not varns:
                lookupfn = self._lookupvar
            else:
                lookupfn = lookup_varns(varns)
            varvalue = lookupfn(varname)
        except KeyError:
            if default is _MARKER:
                raise KeyError("Variable %r not found" % varname)
            else:
                return default
        else:
            return varvalue

    def getvar_s(self, varname, default=_MARKER):
        """Get a variable - including variables from other namespaces.

        `varname` is interpreted as in :meth:`.getvar`. But variables
        will be interpolated recursively within the variable values
        and filters are applied.

        For more details see chapter :ref:`variable-interpolation`.

        """
        try:
            obj = self.getvar(varname)
            return self.substitute_variables_in_obj(obj)
        except KeyError:
            if default is _MARKER:
                raise
            else:
                return default

    def getintvar_s(self, varname, default=_MARKER):
        """Get a (possibly substituted) variable and coerce text to a
        number.

        """
        s = self.getvar_s(varname, default)
        if isinstance(s, self._TEXTTYPE):
            return int(s, 0)
        else:
            return s

    def getboolvar_s(self, varname, default=_MARKER):
        """Get a (possibly substituted) variable and convert text to a
        boolean

        """
        s = self.getvar_s(varname, default)
        if isinstance(s, self._TEXTTYPE):
            sl = s.strip().lower()
            if sl not in self._BOOL_CVT:
                raise ValueError("Not a boolean: %r" % s)
            return self._BOOL_CVT[sl]
        else:
            return s

    # Conversion of booleans
    _BOOL_CVT = {'1': True, 'yes': True, 'true': True, 'on': True,
                 '0': False, 'no': False, 'false': False, 'off': False}

    def getfloatvar_s(self, varname, default=_MARKER):
        """Get a (possibly substituted) variable and convert text to a
        float

        """
        s = self.getvar_s(varname, default)
        if isinstance(s, self._TEXTTYPE):
            return float(s)
        else:
            return s

    def _split_ns(self, s):
        nameparts = s.split(':', 1)
        if len(nameparts) == 1:
            return (None, s, )
        else:
            return (nameparts[0], nameparts[1], )

    def _split_filters(self, s):
        nameparts = s.split('|')
        if len(nameparts) == 1:
            return (s, [], )
        else:
            return (nameparts[0].rstrip(), nameparts[1:], )

    def _lookupvar(self, key, default=_MARKER):
        """Lookup a variable.

        If no default is given an unexisting `key` raises a `KeyError`
        else `default` is returned.
        """
        parts = key.split('.')
        try:
            v = self[parts[0]]
            for p in parts[1:]:
                v = v[p]
        except TypeError:
            raise KeyError(
                "Configuration variable %r not found"
                "(missing intermediate keys?)" % key)
        except KeyError:
            if default is _MARKER:
                raise KeyError("Configuration variable %r not found" % key)
            else:
                return default
        return v

    # Speed
    _TEXTTYPE = type(u(""))

    def substitute_variables_in_obj(self, obj):
        """Recursively expand variables in the object tree `obj`."""
        if isinstance(obj, self._TEXTTYPE):
            # a string - really replace the value
            return self.expand_variable(obj)
        elif isinstance(obj, list):
            return [self.substitute_variables_in_obj(i) for i in obj]
        elif isinstance(obj, tuple):
            tmp = [self.substitute_variables_in_obj(i) for i in obj]
            return type(obj)(tmp)
        elif isinstance(obj, dict):
            newdict = type(obj)()
            for k in obj:
                newdict[k] = self.substitute_variables_in_obj(obj[k])
            return newdict
        elif isinstance(obj, set):
            newset = type(obj)()
            for i in obj:
                newset.add(self.substitute_variables_in_obj(i))
        else:
            return obj

    # Speed
    _STARTTOK = u(b"{{")
    _ENDTOK = u(b"}}")

    def expand_variable(self, s):
        """Expand variables in a single string"""
        start = s.find(self._STARTTOK, 0)
        while start != -1:
            end = s.find(self._ENDTOK, start)
            if end < 0:
                return s
            varname, filters = self._split_filters(s[start+2:end])
            try:
                varvalue = self._apply_filters(filters, self.getvar_s(varname))
            except KeyError:
                warnings.warn("Cannot expand variable %r in string "
                              "%r" % (varname, s, ),
                              UserWarning,
                              stacklevel=1)
                raise
            if varvalue is None:
                varvalue = u("")
            #
            # Dont apply and type conversions to str if the whole `s` is
            # just one expansion
            #
            if (start == 0) and (end + 2 == len(s)):
                return varvalue
            replaced = s[:start] + u(str(varvalue))
            s = replaced + s[end+2:]
            # don't re-evaluate because `self.getvar_s()` expands already
            start = s.find(self._STARTTOK, len(replaced))
        return s

    def _apply_filters(self, filters, value):
        for name in filters:
            try:
                filterfn = lookup_filter(name)
            except KeyError:
                #
                # Convert to NameError because we find a missing filters
                # a very serious error.
                #
                raise NameError("Filter %r not found" % name)
            else:
                value = filterfn(self, value)
        return value