Mercurial > hgrepos > Python > libs > ConfigMix
view configmix/config.py @ 654:0d6673d06c2c
Add support for using "tomllib" (in Python's stdlib since 3.11) and "tomli" TOML packages.
They are preferred if they are found to be installed.
But note that the declared dependency for the "toml" extra nevertheless
is the "toml" package. Because it is available for all supported Python
versions.
So use Python 3.11+ or install "tomli" manually if you want to use the
alternate packages.
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Thu, 19 May 2022 22:10:59 +0200 |
| parents | e8f3e970e411 |
| children | b74f20e19c01 |
line wrap: on
line source
# -*- coding: utf-8 -*- # :- # :Copyright: (c) 2015-2022, 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, n, str_and_u, PY2 from .constants import REF_NAMESPACE, NONE_FILTER, EMPTY_FILTER, DEL_VALUE try: from ._speedups import (fast_unquote, fast_quote, fast_pathstr2path, _fast_split_ns, _fast_split_filters, _fast_getvarl, _fast_getvarl_s, _fast_getvar, _fast_getvar_s, _fast_interpolate_variables, _sync_MISSING, _sync_MARKER) except ImportError: fast_unquote = None fast_quote = None fast_pathstr2path = None _fast_split_ns = None _fast_split_filters = None _fast_getvarl = None _fast_getvarl_s = None _fast_getvar = None _fast_getvar_s = None _fast_interpolate_variables = None _sync_MISSING = None _MARKER = object() _MISSING = 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 CoercingMethodsMixin(object): """Mixin to provide some common implementations for retrieval methods that convert return values to a fixed type (int, bool, float). Both :class:`~.Configuration` and :class:`~._JailedConfiguration` use this mixin. """ def getintvarl_s(self, *path, **kwds): """Get a (possibly substituted) variable and coerce text to a number. """ s = self.getvarl_s(*path, **kwds) if isinstance(s, _TEXTTYPE): return int(s, 0) else: return s def getfirstintvarl_s(self, *paths, **kwds): """Get a (possibly substituted) variable and coerce text to a number. """ s = self.getfirstvarl_s(*paths, **kwds) if isinstance(s, _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, _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, _TEXTTYPE): return int(s, 0) else: return s def getboolvarl_s(self, *path, **kwds): """Get a (possibly substituted) variable and convert text to a boolean """ s = self.getvarl_s(*path, **kwds) if isinstance(s, _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 getfirstboolvarl_s(self, *paths, **kwds): """Get a (possibly substituted) variable and convert text to a boolean """ s = self.getfirstvarl_s(*paths, **kwds) if isinstance(s, _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, _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, _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, *path, **kwds): """Get a (possibly substituted) variable and convert text to a float """ s = self.getvarl_s(*path, **kwds) if isinstance(s, _TEXTTYPE): return float(s) else: return s def getfirstfloatvarl_s(self, *path, **kwds): """Get a (possibly substituted) variable and convert text to a float """ s = self.getfirstvarl_s(*path, **kwds) if isinstance(s, _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, _TEXTTYPE): return float(s) else: return s def getfirstfloatvar_s(self, varname, default=_MARKER): """Get a (possibly substituted) variable and convert text to a float """ s = self.getfirstvar_s(varname, default) if isinstance(s, _TEXTTYPE): return float(s) else: return s # Speed _EMPTY_STR = u("") _TEXTTYPE = type(_EMPTY_STR) _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'%') _QUOTE_x = u(b'x') _QUOTE_u = u(b'u') _QUOTE_U = u(b'U') _COMMENT = u(b'#') _QUOTE_MAP = { 0x25: u(b'%x25'), # _QUOTE 0x2e: u(b'%x2e'), # _DOT 0x3a: u(b'%x3a'), # _NS_SEPARATOR 0x23: u(b'%x23'), # _COMMENT / anchor 0x7c: u(b'%x7c'), # _FILTER_SEPARATOR 0x22: u(b'%x22'), 0x27: u(b'%x27'), 0x7b: u(b'%x7b'), 0x7d: u(b'%x7d'), 0x5b: u(b'%x5b'), 0x5d: u(b'%x5d'), } _QUOTE_SAFE = u(b'abcdefghijklmnopqrstuvwxyz' b'ABCDEFGHIJKLMNOPQRSTUVWXYZ' b'0123456789' b'-_@!$&/\\()=?*+~;,<>^') """Mostly used configuration key characters that do not need any quoting """ def py_quote(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. """ # Quick check whether all of the chars are in _QUOTE_SAFE if not s.lstrip(_QUOTE_SAFE): return s # Slow path re_encode = False if PY2: # Use the Unicode translation variant in PY2 if isinstance(s, str): s = s.decode("latin1") re_encode = True s = s.translate(_QUOTE_MAP) if re_encode: return s.encode("latin1") else: return s if fast_quote: quote = fast_quote else: quote = py_quote def py_unquote(s): """Unquote the content of `s`: handle all patterns ``%xNN``, ``%uNNNN`` or ``%UNNNNNNNN``. This is the inverse of :func:`.quote`. """ if _QUOTE not in s: return s parts = s.split(_QUOTE) res = [parts[0]] res_append = res.append for p in parts[1:]: try: qc = p[0] except IndexError: raise ValueError("unknown quote syntax string: {}".format(s)) if qc == _QUOTE_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 qc == _QUOTE_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 qc == _QUOTE_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 _EMPTY_STR.join(res) if fast_unquote: unquote = fast_unquote else: unquote = py_unquote def py_pathstr2path(varname): """Parse a dot-separated path string `varname` into a tuple of unquoted path items :param str varname: The quoted and dot-separated path string :return: The unquoted and parsed path items :rtype: tuple Used e.g. by :meth:`~.Configuration.getvar`, :meth:`~.Configuration.getvar_s` and :meth:`~.Configuration.jailed`. The returned value is suitable as input for :meth:`~.Configuration.getvarl`, :meth:`~.Configuration.getvarl_s` and friends. An empty `varname` returns an empty tuple. """ # # Because str.split yields a non-empty list for an empty string handle # the empty string separately. # if varname: return tuple([unquote(p) for p in varname.split(_HIER_SEPARATOR)]) else: return tuple() if fast_pathstr2path: pathstr2path = fast_pathstr2path else: pathstr2path = py_pathstr2path def _py_split_ns(varname): """Split the variable name string `varname` into the namespace and the namespace-specific name :type varname: str :return: A tuple containing the namespace (or `None`) and the namespace-specific (variable-)name :rtype: tuple(str or None, str) .. note:: The returned namespace may be an empty string if the namespace separator is found. """ ns, sep, rest = varname.partition(_NS_SEPARATOR) if sep: return (unquote(ns), rest) else: return (None, ns) if _fast_split_ns: _split_ns = _fast_split_ns else: _split_ns = _py_split_ns def _py_split_filters(varname): """Split off the filter part from the `varname` string :type varname: str :return: The tuple of the variable name without the filters and a list of filters :rtype: tuple(str, list) """ name, sep, filters = varname.partition(_FILTER_SEPARATOR) if sep: filters = filters.strip() if filters: return (name, filters.split(_FILTER_SEPARATOR)) else: return (name, []) else: return (name, []) if _fast_split_filters: _split_filters = _fast_split_filters else: _split_filters = _py_split_filters class Configuration(CoercingMethodsMixin, _AttributeDict): """The configuration dictionary with attribute support or variable substitution. .. note:: When retrieving by attribute names variables will *not* substituted. """ is_jail = False """Flag to show that this is not a jail for another configuration""" def __init__(self, *args, **kwds): # # PY2.7 compat: must be set before calling the superclass' __init__ # self.enable_cache() super(Configuration, self).__init__(*args, **kwds) def clear_cache(self): """Clear the internal lookup cache and the interpolation cache""" if self.__lookup_cache is not None: self.__lookup_cache.clear() if self.__interpolation_cache is not None: self.__interpolation_cache.clear() def disable_cache(self): self.__lookup_cache = None self.__interpolation_cache = None def enable_cache(self): self.__lookup_cache = {} self.__interpolation_cache = {} def __getitem__(self, key): """Mapping and list interface that forwards to :meth:`~.getvarl_s` """ if isinstance(key, (tuple, list)): return self.getvarl_s(*key) else: return self.getvarl_s(key) def get(self, key, default=None): """Mapping interface that forwards to :meth:`~.getvarl_s` """ if isinstance(key, (tuple, list)): return self.getvarl_s(*key, default=default) else: return self.getvarl_s(key, default=default) def __contains__(self, key): """Containment test""" if isinstance(key, (tuple, list)): # No namespace and quoting support here try: self._lookupvar(*key) except KeyError: return False else: return True else: return super(Configuration, self).__contains__(key) def getitem_ns(self, key): """Just forward to the *original* :meth:`dict.__getitem__`. No variable interpolation and key path access. """ return super(Configuration, self).__getitem__(key) def items(self): """Items without interpolation""" for k in self: yield (k, self.getitem_ns(k)) def values(self): """Values without interpolation""" for k in self: yield self.getitem_ns(k) def py_getvarl(self, *path, **kwds): """Get a variable where the hierarchy is given in `path` as sequence and the namespace is given in the `namespace` keyword argument. No variable interpolation is done and no filters are applied. Quoting of `path` 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(*path) except KeyError: if default is _MARKER: raise KeyError("Variable %r not found" % (path,)) else: return default else: return varvalue if _fast_getvarl: def fast_getvarl(self, *args, **kwds): return _fast_getvarl(self, args, **kwds) getvarl = fast_getvarl else: getvarl = py_getvarl def getkeysl(self, *path, **kwds): """Yield the keys of a variable value. :rtype: A generator :raise KeyError: .. note:: Dictionary keys are not subject to interpolation. """ if "default" in kwds: raise TypeError("got unexpected keyword argument: default") for k in self.getvarl(*path, **kwds).keys(): yield k def getfirstvarl(self, *paths, **kwds): """A variant of :meth:`~.getvarl` that returns the first found variable in the `paths` list. Every item in `paths` is either a `tuple` or `list` or a `dict`. If the path item is a `dict` then it must have two keys "namespace" and "path". If the path item is a `list` or `tuple` then the namespace is assumed to be `None`. Note that a caller that wants to use variables from a non-default namespace must use a sequence of dicts. No variable interpolation is done and no filters are applied. Quoting of anything in `paths` is *not* needed and wrong. """ default = kwds.pop("default", _MARKER) for path in paths: if isinstance(path, (list, tuple)): try: varvalue = self.getvarl(*path) except KeyError: pass else: return varvalue elif isinstance(path, dict): try: namespace = path["namespace"] p = path["path"] except KeyError: raise TypeError("a paths dict item must have a `path'" " and a `namespace' key") else: try: varvalue = self.getvarl(*p, namespace=namespace) except KeyError: pass else: return varvalue else: raise TypeError("a paths item must be a dict, list or tuple") if default is _MARKER: raise KeyError( "none of the given variables found: %r" % (paths,)) else: return default def py_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 :func:`.quote`. """ varns, varname = _split_ns(varname) if not varns: return self.getvarl(*pathstr2path(varname), default=default) else: return self.getvarl(varname, namespace=varns, default=default) if _fast_getvar: def fast_getvar(self, varname, default=_MARKER): return _fast_getvar(self, varname, default) getvar = fast_getvar else: getvar = py_getvar def getkeys(self, varname): """Yield all the keys of a variable value. :rtype: A generator :raise KeyError: .. note:: Dictionary keys are not subject to interpolation. """ for k in self.getvar(varname).keys(): yield k 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 py_getvarl_s(self, *path, **kwds): """Get a variable - including variables from other namespaces. `path` 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(*path, namespace=namespace) return self.substitute_variables_in_obj(obj) except KeyError: if default is _MARKER: raise else: return default if _fast_getvarl_s: def fast_getvarl_s(self, *path, **kwds): return _fast_getvarl_s(self, path, **kwds) getvarl_s = fast_getvarl_s else: getvarl_s = py_getvarl_s def getfirstvarl_s(self, *paths, **kwds): """A variant of :meth:`~.getfirstvarl` that does variable interpolation. `paths` and `kwds` are interpreted as in :meth:`.getfirstvarl`. 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) for path in paths: if isinstance(path, (list, tuple)): try: obj = self.getvarl(*path) except KeyError: pass else: return self.substitute_variables_in_obj(obj) elif isinstance(path, dict): try: namespace = path["namespace"] p = path["path"] except KeyError: raise TypeError("a paths dict item must have a `path'" " and a `namespace' key") else: try: obj = self.getvarl(*p, namespace=namespace) except KeyError: pass else: return self.substitute_variables_in_obj(obj) else: raise TypeError("a paths item must be a dict, list or tuple") if default is _MARKER: raise KeyError( "none of the given variables found: %r" % (paths,)) else: return default def py_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`. """ varns, varname = _split_ns(varname) try: if not varns: return self.substitute_variables_in_obj( self.getvarl(*pathstr2path(varname))) else: return self.substitute_variables_in_obj( self.getvarl(varname, namespace=varns)) except KeyError: if default is _MARKER: raise else: return default if _fast_getvar_s: def fast_getvar_s(self, varname, default=_MARKER): return _fast_getvar_s(self, varname, default) getvar_s = fast_getvar_s else: getvar_s = py_getvar_s def _py_getvar_s_with_cache_info(self, varname): """Internal variant of :meth:`~.getvar_s` that returns some information whether caching of interpolated values is allowed Caching is currently not allowed when namespaces are used. Currently used by :meth:`~.interpolate_variables`. """ varns, varname = _split_ns(varname) if not varns: # no namespace -> cacheable return ( self.substitute_variables_in_obj( self.getvarl(*pathstr2path(varname))), True ) else: # results from namespaced lookups are currently not cacheable return ( self.substitute_variables_in_obj( self.getvarl(varname, namespace=varns)), False ) 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 _lookupvar(self, *path): """Lookup a variable within a hierarchy. :raise KeyError: An unexisting `path` raises a `KeyError` """ if not path: return self use_cache = self.__lookup_cache is not None if use_cache: v = self.__lookup_cache.get(path, _MARKER) if v is not _MARKER: if v is _MISSING: raise KeyError( "Configuration variable %r not found" " (negative internal cache value)" % (path,)) else: return v eiref = self.expand_if_reference try: v = eiref(super(Configuration, self).__getitem__(path[0])) for p in path[1:]: v = eiref(v[p]) except TypeError: if use_cache: self.__lookup_cache[path] = _MISSING raise KeyError( "Configuration variable %r not found" "(missing intermediate keys?)" % (path,)) except KeyError: if use_cache: self.__lookup_cache[path] = _MISSING raise if use_cache: self.__lookup_cache[path] = v return v def _lookupref(self, key): """ `key` must be a configuration reference URI without any (namespace) prefixes and suffixes :raise KeyError: If the reference is not found """ return self.expand_ref_uri(key) def expand_if_reference(self, v): """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. :raise KeyError: If the reverence cannot found """ if not isinstance(v, _TEXTTYPE): return v if v.startswith(_STARTTOK_REF) and v.endswith(_ENDTOK_REF): return self.expand_ref_uri( v[len(_STARTTOK_REF):-len(_ENDTOK_REF)]) else: return v def expand_ref_uri(self, uri): """ :raises KeyError: If the reference URI is not found """ 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(_DOT): raise ValueError("relative refs not supported") return self.getvar(pu.fragment) def substitute_variables_in_obj(self, obj): """Recursively expand variables in the object tree `obj`.""" ty = type(obj) if issubclass(ty, _TEXTTYPE): # a string - really replace the value return self.interpolate_variables(obj) elif issubclass(ty, dict): newdict = ty() for k, v in obj.items(): newdict[k] = self.substitute_variables_in_obj(v) return newdict elif issubclass(ty, list): return [self.substitute_variables_in_obj(i) for i in obj] elif issubclass(ty, tuple): return ty([self.substitute_variables_in_obj(i) for i in obj]) elif issubclass(ty, set): newset = ty() for i in obj: newset.add(self.substitute_variables_in_obj(i)) else: return obj def py_interpolate_variables(self, s): """Expand all variables in the single string `s`""" len_s = len(s) if len_s < 4: return s if s == DEL_VALUE: return s start = s.find(_STARTTOK, 0) if start < 0: return s use_cache = self.__interpolation_cache is not None if use_cache: res = self.__interpolation_cache.get(s, _MARKER) if res is not _MARKER: if res is _MISSING: warnings.warn("Cannot interpolate variables in string " "%r (cached)" % (s, ), UserWarning, stacklevel=1) raise KeyError("Cannot interpolate variables in string " "%r (cached)" % (s, )) else: return res res = [] res_append = res.append rest = 0 cacheable = True while start != -1: res_append(s[rest:start]) end = s.find(_ENDTOK, start) if end < 0: rest = start break varname, filters = _split_filters( s[start+2:end]) # noqa: E226 try: varvalue, cacheable = self._py_getvar_s_with_cache_info(varname) except KeyError: cacheable = True if NONE_FILTER in filters: varvalue = None elif EMPTY_FILTER in filters: varvalue = _EMPTY_STR else: if use_cache and cacheable: self.__interpolation_cache[s] = _MISSING warnings.warn("Cannot interpolate variable %r in string " "%r" % (varname, s, ), UserWarning, stacklevel=1) raise if not cacheable: cacheable = False varvalue = self._apply_filters(filters, varvalue) rest = end + 2 # # Dont apply and type conversions to the variable value if # the whole `s` is just one expansion # if (start == 0) and (rest == len_s): if use_cache and cacheable: self.__interpolation_cache[s] = varvalue return varvalue if varvalue is None: pass else: res_append(str_and_u(varvalue)) # don't re-evaluate because `self.getvar_s()` expands already start = s.find(_STARTTOK, rest) res_append(s[rest:]) res = _EMPTY_STR.join(res) if use_cache and cacheable: self.__interpolation_cache[s] = res return res if _fast_interpolate_variables: def fast_interpolate_variables(self, s): return _fast_interpolate_variables( self, s, self.__interpolation_cache) interpolate_variables = fast_interpolate_variables else: interpolate_variables = py_interpolate_variables 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 def jailed(self, rootpath=None, root=None, bind_root=True): """Return a "jailed" configuration of the current configuration. :param rootpath: a sequence of strings that shall emcompass the chroot-like jail of the returned configuration :type rootpath: list or tuple :param str root: a string path expression that shall encompass the chroot-like jail of the returned configuration :param bool bind_root: if you do a :meth:`~.rebind` just after creation of a jailed config you can set `bind_root` to `False`; otherwise use the default :return: a jailed (aka restricted) configuration :rtype: _JailedConfiguration Exactly one of `rootpath` or `root` must be given. """ if rootpath is not None and root is not None: raise ValueError("only one of `rootpath' or `root' can be given") if rootpath is None and root is None: raise ValueError("one of `rootpath' or `root' must be given") if rootpath is not None and not isinstance(rootpath, (list, tuple)): raise TypeError("`rootpath' must be a list or a tuple") if root is not None: # convert to path varns, varname = _split_ns(root) if varns: raise ValueError( "jailed configurations do not support namespaces") rootpath = pathstr2path(root) jc = _JailedConfiguration(*rootpath) if bind_root: jc.rebind(self) return jc class _JailedConfiguration(CoercingMethodsMixin): """A jailed and restricted variant of :class:`Configuration`. Restriction is two-fold: - The access to configuration variables in `config` is restricted to the configuration sub-tree that is configured in `path`. - Not all access-methods of :class:`Configuration` are implemented yet. .. seealso:: :ref:`jailed-configuration` .. note:: There is no namespace support. .. note:: Do not call the constructor directly. Instantiate a jailed configuration from the parent configuration's :meth:`~.Configuration.jailed` factory method. """ __slots__ = ("_base", "_path", "_path_string") is_jail = True """Flag to show that this is a jail for another configuration""" def __init__(self, *path): super(_JailedConfiguration, self).__init__() self._path = path self._path_string = None @property def _pathstr(self): v = self._path_string if v is None: if self._path: v = _HIER_SEPARATOR.join([quote(p) for p in self._path]) \ + _HIER_SEPARATOR else: v = _EMPTY_STR self._path_string = v return v @property def base(self): """Ask for the base (aka parent) configuration". This configuration is always unjailed. """ return self._base def rebind(self, new_base): """Bind the jail to a new unjailed configuration `new_base`. The new configuration base also must have an existing path to the root. :param Configuration new_base: the new base """ if new_base.is_jail: raise TypeError("can only bind to an unjailed configuration") self._base = new_base # # Early error out if the chroot does not exist but allow # degenerated case if `self._path` is empty. # if self._path and self._path not in new_base: raise KeyError( "base key path %r not available in the new base" % (self._path, )) def __getattr__(self, name): """Attribute-style access. Result values are interpolated (i.e. forwarded to :meth:`~.getvarl_s`) """ try: v = self._base.getvarl_s(*(self._path + (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 def __getitem__(self, key): """Mapping and list interface that forwards to :meth:`~.getvarl_s` """ if isinstance(key, tuple): return self._base.getvarl_s(*(self._path + key)) elif isinstance(key, list): return self._base.getvarl_s(*(self._path + tuple(key))) else: return self._base.getvarl_s(*self._path)[key] def get(self, key, default=None): if isinstance(key, tuple): return self._base.get(self._path + key, default=default) elif isinstance(key, list): return self._base.get(self._path + tuple(key), default=default) else: return self._base.get(self._path + (key, ), default=default) def __contains__(self, key): """Containment support for containers""" if isinstance(key, tuple): return (self._path + key) in self._base elif isinstance(key, list): return (self._path + tuple(key)) in self._base else: return (self._path + (key, )) in self._base def getvarl(self, *path, **kwds): return self._base.getvarl(*(self._path + path), **kwds) def getkeysl(self, *path, **kwds): for k in self._base.getkeysl(*(self._path + path), **kwds): yield k def getfirstvarl(self, *paths, **kwds): real_paths = [] for path in paths: if isinstance(path, (list, tuple)): real_paths.append(self._path + tuple(path)) elif isinstance(path, dict): raise TypeError( "a `dict' is not supported in a jailed configuration") else: raise TypeError("a paths item must be a list or tuple") return self._base.getfirstvarl(*real_paths, **kwds) def getvarl_s(self, *path, **kwds): return self._base.getvarl_s(*(self._path + path), **kwds) def getfirstvarl_s(self, *paths, **kwds): real_paths = [] for path in paths: if isinstance(path, (list, tuple)): real_paths.append(self._path + tuple(path)) elif isinstance(path, dict): raise TypeError( "a `dict' is not supported in a jailed configuration") else: raise TypeError("a paths item must be a list or tuple") return self._base.getfirstvarl_s(*real_paths, **kwds) def getvar(self, varname, **kwds): return self._base.getvarl(*(self._path + pathstr2path(varname)), **kwds) def getkeys(self, varname): for k in self._base.getkeysl(*(self._path + pathstr2path(varname))): yield k def getfirstvar(self, *varnames, **kwds): real_varnames = [self._pathstr + vn for vn in varnames] return self._base.getfirstvar(*real_varnames, **kwds) def getvar_s(self, varname, **kwds): return self._base.getvarl_s(*(self._path + pathstr2path(varname)), **kwds) def getfirstvar_s(self, *varnames, **kwds): real_varnames = [self._pathstr + vn for vn in varnames] return self._base.getfirstvar_s(*real_varnames, **kwds) def __iter__(self): """Iteration support for containers""" return iter(self._base.getvarl_s(*self._path)) def __len__(self): """Length support for containers""" return len(self._base.getvarl_s(*self._path)) if PY2: def __nonzero__(self): """Map- and list-style evaluation in boolean context""" return bool(self._base.getvarl_s(*self._path)) else: def __bool__(self): """Map- and list-style evaluation in boolean context""" return bool(self._base.getvarl_s(*self._path)) def jailed(self, rootpath=None, root=None, bind_root=True): """Return a "jailed" configuration that effectively is a subjail of the current jail For a more complete description see :meth:`.Configuration.jailed`. """ if rootpath is not None and root is not None: raise ValueError("only one of `rootpath' or `root' can be given") if rootpath is None and root is None: raise ValueError("one of `rootpath' or `root' must be given") if rootpath is not None and not isinstance(rootpath, (list, tuple)): raise TypeError("`rootpath' must be a list or a tuple") if root is not None: # convert to path varns, varname = _split_ns(root) if varns: raise ValueError( "sub-jails do not support namespaces") rootpath = pathstr2path(varname) if self._path: new_rootpath = self._path + tuple(rootpath) else: new_rootpath = tuple(rootpath) sjc = _JailedConfiguration(*new_rootpath) if bind_root: sjc.rebind(self._base) return sjc def __repr__(self): r = "_JailedConfiguration(rootpath=%s)" % n(repr(self._path)) return r if _sync_MISSING: _sync_MISSING(_MISSING) _sync_MARKER(_MARKER)
