Mercurial > hgrepos > Python > libs > ConfigMix
view configmix/config.py @ 380:bb4a90fb58e0
Docu: "namespace" in getvarl() is a keyword argument
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Tue, 09 Nov 2021 21:58:05 +0100 |
| parents | 4d7ad20cb8f9 |
| children | fe3dfd687621 |
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, uchr from .constants import REF_NAMESPACE, NONE_FILTER, EMPTY_FILTER _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 retrieving by attribute names variables will *not* substituted. """ # Speed _TEXTTYPE = type(u("")) _STARTTOK = u(b"{{") _ENDTOK = u(b"}}") _HIER_SEPARATOR = u(b'.') _NS_SEPARATOR = u(b':') _FILTER_SEPARATOR = u(b'|') _STARTTOK_REF = _STARTTOK + REF_NAMESPACE + _NS_SEPARATOR _ENDTOK_REF = _ENDTOK _DOT = u(b'.') _QUOTE = u(b'%') _COMMENT = u(b'#') def getvarl(self, *names, **kwds): """Get a variable where the hierarchy is given in `names` as sequence and the namespace is given in the `namespace` keyword argument. No variable interpolation is done and no filters are applied. Quoting of `names` and `namespace` is *not* needed and wrong. """ default = kwds.pop("default", _MARKER) namespace = kwds.pop("namespace", None) try: if not namespace: lookupfn = self._lookupvar else: if namespace == REF_NAMESPACE: lookupfn = self._lookupref else: lookupfn = lookup_varns(namespace) varvalue = lookupfn(*names) except KeyError: if default is _MARKER: raise KeyError("Variable %r not found" % (names,)) else: return default else: return varvalue 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. Special characters (e.g. ``:`` and ``.``) must be quoted when using the default namespace. See also :meth:`~.quote`. """ varns, varname = self._split_ns(varname) if not varns: varnameparts = [self.unquote(vp) for vp in varname.split(self._HIER_SEPARATOR)] else: varnameparts = (varname,) return self.getvarl(*varnameparts, namespace=varns, default=default) def getfirstvar(self, *varnames, **kwds): """A variant of :meth:`~.getvar` that returns the first found variable in the list of given variables in `varnames`. """ default = kwds.pop("default", _MARKER) for varname in varnames: try: varvalue = self.getvar(varname) except KeyError: pass else: return varvalue if default is _MARKER: raise KeyError( "none of the given variables found: %r" % (varnames,)) else: return default def getvarl_s(self, *names, **kwds): """Get a variable - including variables from other namespaces. `names` and `namespace` are interpreted as in :meth:`.getvarl`. But variables will be interpolated recursively within the variable values and filters are applied. For more details see chapter :ref:`variable-interpolation`. """ default = kwds.pop("default", _MARKER) namespace = kwds.pop("namespace", None) try: obj = self.getvarl(*names, namespace=namespace) return self.substitute_variables_in_obj(obj) except KeyError: if default is _MARKER: raise else: return default 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 getfirstvar_s(self, *varnames, **kwds): """A variant of :meth:`~.getvar_s` that returns the first found variable in the list of given variables in `varnames`. """ default = kwds.pop("default", _MARKER) for varname in varnames: try: obj = self.getvar(varname) except KeyError: pass else: return self.substitute_variables_in_obj(obj) if default is _MARKER: raise KeyError( "none of the given variables found: %r" % (varnames,)) else: return default def getintvarl_s(self, *names, **kwds): """Get a (possibly substituted) variable and coerce text to a number. """ s = self.getvarl_s(*names, **kwds) if isinstance(s, self._TEXTTYPE): return int(s, 0) else: return s def getintvar_s(self, varname, default=_MARKER): """Get a (possibly substituted) variable and coerce text to a number. """ s = self.getvar_s(varname, default=default) if isinstance(s, self._TEXTTYPE): return int(s, 0) else: return s def getfirstintvar_s(self, *varnames, **kwds): """A variant of :meth:`~.getintvar_s` that returns the first found variable in the list of given variables in `varnames`. """ s = self.getfirstvar_s(*varnames, **kwds) if isinstance(s, self._TEXTTYPE): return int(s, 0) else: return s def getboolvarl_s(self, *names, **kwds): """Get a (possibly substituted) variable and convert text to a boolean """ s = self.getvarl_s(*names, **kwds) 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 def getboolvar_s(self, varname, default=_MARKER): """Get a (possibly substituted) variable and convert text to a boolean """ s = self.getvar_s(varname, default=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 def getfirstboolvar_s(self, *varnames, **kwds): """A variant of :meth:`~.getboolvar_s` that returns the first found variable in the list of given variables in `varnames`. """ s = self.getfirstvar_s(*varnames, **kwds) 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 getfloatvarl_s(self, *names, **kwds): """Get a (possibly substituted) variable and convert text to a float """ s = self.getvarl_s(*names, **kwds) if isinstance(s, self._TEXTTYPE): return float(s) else: return s 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 (self.unquote(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, *names, **kwds): """Lookup a variable within a hierarchy. If no default is given an unexisting `name` raises a `KeyError` else `default` is returned. """ default = kwds.pop("default", _MARKER) try: v = self.expand_if_reference(self[names[0]]) for p in names[1:]: v = self.expand_if_reference(v[p]) except TypeError: raise KeyError( "Configuration variable %r not found" "(missing intermediate keys?)" % (names,)) except KeyError: if default is _MARKER: raise KeyError( "Configuration variable %r not found" % (names,)) else: return default return v def _lookupref(self, key, default=_MARKER): """ `key` must be a configuration 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 configuration 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: if NONE_FILTER in filters: varvalue = self._apply_filters( filters, self.getvar_s(varname, default=None)) elif EMPTY_FILTER in filters: varvalue = self._apply_filters( filters, self.getvar_s(varname, default=u(""))) else: 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 # # Dont apply and type conversions to the variable value if # the whole `s` is just one expansion # if (start == 0) and (end + 2 == len(s)): return varvalue if varvalue is None: varvalue = u("") 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 @classmethod def quote(klass, s): """Replace important special characters in string `s` by replacing them with ``%xNN`` where `NN` are the two hexadecimal digits of the characters unicode codepoint value. Handled are the important special chars: ``%``, ``.``, ``:``, ``#``; ``'``, ``"``, ``|``, ``{``, ``}``, ``[`` and ``]``. See also the :ref:`quoting` section. """ qc = klass._QUOTE s = s.replace(qc, qc + "x25") s = s.replace(klass._DOT, qc + "x2e") s = s.replace(klass._NS_SEPARATOR, qc + "x3a") s = s.replace(klass._COMMENT, qc + "x23") s = s.replace(klass._FILTER_SEPARATOR, qc + "x7c") s = s.replace('"', qc + "x22") s = s.replace("'", qc + "x27") s = s.replace('{', qc + "x7b") s = s.replace('}', qc + "x7d") s = s.replace('[', qc + "x5b") return s.replace(']', qc + "x5d") @classmethod def unquote(klass, s): """Unquote the content of `s`: handle all patterns ``%xNN``, ``%uNNNN`` or ``%UNNNNNNNN``. This is the inverse of :meth:`~.quote`. """ if klass._QUOTE not in s: return s res = [] parts = s.split(klass._QUOTE) res.append(parts[0]) for p in parts[1:]: if p.startswith(u(b'x')): if len(p) < 3: raise ValueError("quote syntax: length too small") res.append(uchr(int(p[1:3], 16))) res.append(p[3:]) elif p.startswith(u(b'u')): if len(p) < 5: raise ValueError("quote syntax: length too small") res.append(uchr(int(p[1:5], 16))) res.append(p[5:]) elif p.startswith(u(b'U')): if len(p) < 9: raise ValueError("quote syntax: length too small") res.append(uchr(int(p[1:9], 16))) res.append(p[9:]) else: raise ValueError("unknown quote syntax string: {}".format(s)) return ''.join(res)
