view configmix/config.py @ 305:f529ca46dd50

Implemented the "ref" namespace to get configuration tree references. BUGS: - Tests should be done more thoroughly and extensively - Interaction of tree references and variable substitution should be tested more properly - Documentation is missing yet
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 26 Apr 2021 09:42:42 +0200
parents d8361dd70d2d
children 801cd16223e4
line wrap: on
line source

# -*- coding: utf-8 -*-
# :-
# :Copyright: (c) 2015-2021, Franz Glasner. All rights reserved.
# :License:   BSD-3-Clause. 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
try:
    from urllib.parse import urlsplit
except ImportError:
    from urlparse import urlsplit

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


_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.

    """

    # Speed
    _TEXTTYPE = type(u(""))
    _STARTTOK = u(b"{{")
    _ENDTOK = u(b"}}")
    _NS_SEPARATOR = u(b':')
    _FILTER_SEPARATOR = u(b'|')
    _STARTTOK_REF = _STARTTOK + REF_NAMESPACE + _NS_SEPARATOR
    _ENDTOK_REF = _ENDTOK
    _DOT = u(b'.')

    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:
                if varns == REF_NAMESPACE:
                    lookupfn = self._lookupref
                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 = {
        u('1'): True, u('yes'): True, u('true'): True, u('on'): True,
        u('0'): False, u('no'): False, u('false'): False, u('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(self._NS_SEPARATOR, 1)
        if len(nameparts) == 1:
            return (None, s, )
        else:
            return (nameparts[0], nameparts[1], )

    def _split_filters(self, s):
        nameparts = s.split(self._FILTER_SEPARATOR)
        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.expand_if_reference(self[parts[0]])
            for p in parts[1:]:
                v = self.expand_if_reference(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

    def _lookupref(self, key, default=_MARKER):
        """
        `key` must be a reference URI without any (namespace) prefixes
        and suffixes

        """
        return self.expand_ref_uri(key, default=default)

    def expand_if_reference(self, v, default=_MARKER):
        """Check whether `v` is a reference and -- if true -- then expand it.

        `v` must match the pattern ``{{{ref:<REFERENCE>}}}``

        All non-matching texttypes and all non-texttypes are returned
        unchanged.

        """
        if not isinstance(v, self._TEXTTYPE):
            return v
        if not v.startswith(self._STARTTOK_REF) \
                or not v.endswith(self._ENDTOK_REF):
            return v
        return self.expand_ref_uri(
            v[len(self._STARTTOK_REF):-len(self._ENDTOK_REF)],
            default=default)

    def expand_ref_uri(self, uri, default=_MARKER):
        pu = urlsplit(uri)
        if pu.scheme or pu.netloc or pu.path or pu.query:
            raise ValueError("only fragment-only URIs are supported")
        if not pu.fragment:
            return self
        if pu.fragment.startswith(self._DOT):
            raise ValueError("relative refs not supported")
        return self.getvar(pu.fragment, default=default)

    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

    def expand_variable(self, s):
        """Expand variables in the single string `s`"""
        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