view configmix/config.py @ 156:e2e8d21b4122

Adjust copyright to year 2019
author Franz Glasner <fzglas.hg@dom66.de>
date Thu, 21 Feb 2019 22:16:05 +0100
parents a5339d39af5c
children 2e66178a09d8
line wrap: on
line source

# -*- coding: utf-8 -*-
#-
# :Copyright: (c) 2015-2019, 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

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


__all__ = ["Configuration"]


_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 expansion 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 expanded recursively within the variable values and
        filters are applied.

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

        """
        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 not sl 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("")
            replaced = u(b"{0}{1}").format(s[:start], varvalue)
            s = u(b"{0}{1}").format(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