Source code for lib.ExtendedConfigParser

# EMACS settings: -*-	tab-width: 2; indent-tabs-mode: t; python-indent-offset: 2 -*-
# vim: tabstop=2:shiftwidth=2:noexpandtab
# kate: tab-width 2; replace-tabs off; indent-width 2;
#
# ==============================================================================
# Authors:          Patrick Lehmann
#
# Python Module:    Derived and extended configparser from Python standard library
#
# Description:
# ------------------------------------
#   - Improved interpolation algorithm
#   - Added an interpolation cache
#   - Added recursive interpolation (indirect addressing): ${key1.${key2:opt2}:opt1}
#   - Added %{keyword} interpolation, to access the section name: %{parent}
#   - Added support for multiple DEFAULT sections [CONFIG.DEFAULT] for all [CONFIG.**] sections
#
# License:
# ==============================================================================
# Copyright 2007-2016 Patrick Lehmann - Dresden, Germany
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
#
# load dependencies
from itertools    import chain as itertools_chain
from functools    import partial as functools_partial
from re           import compile as re_compile, escape as re_escape, VERBOSE as RE_VERBOSE
from sys          import version_info

from collections  import OrderedDict as _default_dict, ChainMap as _ChainMap, MutableMapping
from configparser import ConfigParser, SectionProxy, Interpolation, MAX_INTERPOLATION_DEPTH, DEFAULTSECT, _UNSET
from configparser import NoSectionError, InterpolationDepthError, InterpolationSyntaxError, NoOptionError, InterpolationMissingOptionError


__api__ = [
	'ExtendedSectionProxy',
	'ExtendedInterpolation',
	'ExtendedConfigParser'
]
__all__ = __api__


[docs]class ExtendedSectionProxy(SectionProxy): def __getitem__(self, key): if not self._parser.has_option(self._name, key): raise KeyError(self._name + ":" + key) return self._parser.get(self._name, key) def __setitem__(self, key, value): super().__setitem__(key, value) self.parser.Interpolation.clear_cache()
# WORKAROUND: Required for ReadTheDocs, which doesn't support Python 3.5 yet. if (version_info < (3,5,0)): class ConverterMapping(MutableMapping): """Enables reuse of get*() methods between the parser and section proxies. If a parser class implements a getter directly, the value for the given key will be ``None``. The presence of the converter name here enables section proxies to find and use the implementation on the parser class. """ GETTERCRE = re_compile(r"^get(?P<name>.+)$") def __init__(self, parser): self._parser = parser self._data = {} for getter in dir(self._parser): m = self.GETTERCRE.match(getter) if not m or not callable(getattr(self._parser, getter)): continue self._data[m.group('name')] = None # See class docstring. def __getitem__(self, key): return self._data[key] def __setitem__(self, key, value): try: k = 'get' + key except TypeError: raise ValueError('Incompatible key: {} (type: {})' ''.format(key, type(key))) if k == 'get': raise ValueError('Incompatible key: cannot use "" as a name') self._data[key] = value func = functools_partial(self._parser._get_conv, conv=value) func.converter = value setattr(self._parser, k, func) for proxy in self._parser.values(): getter = functools_partial(proxy.get, _impl=func) setattr(proxy, k, getter) def __delitem__(self, key): try: k = 'get' + (key or None) except TypeError: raise KeyError(key) del self._data[key] for inst in itertools_chain((self._parser,), self._parser.values()): try: delattr(inst, k) except AttributeError: # don't raise since the entry was present in _data, silently # clean up continue def __iter__(self): return iter(self._data) def __len__(self): return len(self._data) else: from configparser import ConverterMapping # Monkey patching ... (a.k.a. duck punshing import configparser configparser.SectionProxy = ExtendedSectionProxy
[docs]class ExtendedInterpolation(Interpolation): _KEYCRE = re_compile(r"\$\{(?P<ref>[^}]+)\}") _KEYCRE2 = re_compile(r"\$\[(?P<ref>[^\]]+)\}") def __init__(self): self._cache = dict()
[docs] def clear_cache(self): self._cache = dict()
[docs] def before_get(self, parser, section, option, value, defaults): # print("before_get: {0}:{1} = '{2}'".format(section, option, value)) try: result = self.GetCached(section, option) except KeyError: result = self.interpolate(parser, section, option, value, defaults) self.UpdateCache(section, option, result) # print("before_get: => '{0}'\n".format(result)) return result
[docs] def before_set(self, parser, section, option, value): tmp_value = value.replace("$$", "") # escaped dollar signs tmp_value = self._KEYCRE.sub("", tmp_value) # valid syntax if '$' in tmp_value: raise ValueError("invalid interpolation syntax in {0!r} at position {1}".format(value, tmp_value.find("$"))) return value
[docs] def interpolate(self, parser, section, option, value, _, depth=0): if depth > MAX_INTERPOLATION_DEPTH: raise InterpolationDepthError(option, section, value) # short cut operations if empty or a normal string if (value == ""): # print("interpol: SHORT -> empty string") return "" elif (("$" not in value) and ("%" not in value)): # print("interpol: SHORT -> {0}".format(value)) return value # print("interpol: PREPARE section={0} option={1} value='{2}'".format(section, option, value)) rawValue = value rest = "" while (len(rawValue) > 0): beginPos = rawValue.find("%") if (beginPos < 0): rest += rawValue rawValue = "" else: rest += rawValue[:beginPos] if (rawValue[beginPos + 1] == "%"): rest += "%" rawValue = rawValue[1:] elif (rawValue[beginPos + 1] == "{"): endPos = rawValue.find("}", beginPos) if (endPos < 0): raise InterpolationSyntaxError(option, section, "bad interpolation variable reference {0!r}".format(rawValue)) path = rawValue[beginPos + 2:endPos] rawValue = rawValue[endPos + 1:] rest += self.GetSpecial(section, option, path) # print("interpol: BEGIN section={0} option={1} value='{2}'".format(section, option, rest)) result = "" while (len(rest) > 0): # print("interpol: LOOP rest='{0}'".format(rest)) beginPos = rest.find("$") if (beginPos < 0): result += rest rest = "" else: result += rest[:beginPos] if (rest[beginPos + 1] == "$"): result += "$" rest = rest[1:] elif (rest[beginPos + 1] == "{"): endPos = rest.find("}", beginPos) nextPos = rest.rfind("$", beginPos, endPos) if (endPos < 0): raise InterpolationSyntaxError(option, section, "bad interpolation variable reference {0!r}".format(rest)) if ((nextPos > 0) and (nextPos < endPos)): # an embedded $-sign path = rest[nextPos+2:endPos] # print("interpol: path='{0}'".format(path)) innervalue = self.GetValue(parser, section, option, path) # innervalue = self.interpolate(parser, section, option, path, map, depth + 1) # print("interpol: innervalue='{0}'".format(innervalue)) rest = rest[beginPos:nextPos] + innervalue + rest[endPos + 1:] # print("interpol: new rest='{0}'".format(rest)) else: path = rest[beginPos+2:endPos] rest = rest[endPos+1:] result += self.GetValue(parser, section, option, path) # print("interpol: LOOP END - result='{0}'".format(result)) # print("interpol: RESULT => '{0}'".format(result)) return result
@staticmethod
[docs] def GetSpecial(section, option, path): parts = section.split(".") if (path == "Root"): return parts[0] elif (path == "Parent"): return ".".join(parts[1:-1]) elif (path == "ParentWithRoot"): return ".".join(parts[:-1]) elif (path == "GrantParent"): return ".".join(parts[1:-2]) elif (path == "Path"): return ".".join(parts[1:]) elif (path == "PathWithRoot"): return section elif (path == "Name"): return parts[-1] else: raise InterpolationSyntaxError(option, section, "Unknown keyword '{0}'in special operator.".format(path))
[docs] def GetValue(self, parser, section, option, path): path = path.split(":") if (len(path) == 1): sec = section opt = parser.optionxform(path[0]) elif (len(path) == 2): sec = path[0] opt = parser.optionxform(path[1]) else: raise InterpolationSyntaxError(option, section, "More than one ':' found.") try: return self.GetCached(sec, opt) except KeyError: pass try: value = parser.get(sec, opt, raw=True) # print("GetValue: successful parser access: '{0}'".format(value)) except (KeyError, NoSectionError, NoOptionError) as ex: raise InterpolationMissingOptionError(option, section, "", ":".join(path)) from ex if (("$" in value) or ("%" in value)): value = self.interpolate(parser, sec, opt, value, {}) self.UpdateCache(sec, opt, value) return value
[docs] def GetCached(self, section, option): # print("GetCached: {0}:{1}".format(section, option)) if (section not in self._cache): raise KeyError(section) sect = self._cache[section] if (option not in sect): raise KeyError("{0}:{1}".format(section, option)) value = sect[option] # print("GetCached: found: {0}".format(value)) return value
[docs] def UpdateCache(self, section, option, value): # print("UpdateCache: {0}:{1} <- {2}".format(section, option, value)) if (section in self._cache): sect = self._cache[section] if (option in sect): raise Exception("This value is already cached.") sect[option] = value else: self._cache[section] = {option : value}
[docs]class ExtendedConfigParser(ConfigParser): _DEFAULT_INTERPOLATION = ExtendedInterpolation() def __init__(self, defaults=None, dict_type=_default_dict, allow_no_value=False, *, delimiters=('=', ':'), comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=DEFAULTSECT, interpolation=_UNSET, converters=_UNSET): # replacement of ConfigParser.__init__, do not call super-class constructor self._dict = dict_type self._defaults = dict_type() self._sections = dict_type() self._proxies = dict_type() self._cache = dict() self._comment_prefixes = tuple(comment_prefixes or ()) self._inline_comment_prefixes = tuple(inline_comment_prefixes or ()) self._strict = strict self._allow_no_value = allow_no_value self._empty_lines_in_values = empty_lines_in_values self.default_section = default_section self._converters = ConverterMapping(self) if (converters is not _UNSET): self._converters.update(converters) self._proxies[default_section] = SectionProxy(self, default_section) if defaults: for key, value in defaults.items(): self._defaults[self.optionxform(key)] = value self._delimiters = tuple(delimiters) if delimiters == ('=', ':'): self._optcre = self.OPTCRE_NV if allow_no_value else self.OPTCRE else: d = "|".join(re_escape(d) for d in delimiters) if allow_no_value: self._optcre = re_compile(self._OPT_NV_TMPL.format(delim=d), RE_VERBOSE) else: self._optcre = re_compile(self._OPT_TMPL.format(delim=d), RE_VERBOSE) if (interpolation is None): self._interpolation = Interpolation() elif (interpolation is _UNSET): self._interpolation = ExtendedInterpolation() else: self._interpolation = interpolation
[docs] def clear(self): super().clear() if isinstance(self._interpolation, ExtendedInterpolation): self._interpolation.clear_cache()
@property def Interpolation(self): return self._interpolation
[docs] def _unify_values(self, section, variables): """Create a sequence of lookups with 'variables' taking priority over the 'section' which takes priority over the DEFAULTSECT. """ try: sectiondict = self._sections[section] except KeyError: if section != self.default_section: raise NoSectionError(section) else: sectiondict = {} # Update with the entry specific variables vardict = {} if variables: for key, value in variables.items(): if value is not None: value = str(value) vardict[self.optionxform(key)] = value prefix = section.split(".",1)[0] + ".DEFAULT" # print("searched for {0}".format(prefix)) try: defaultdict = self._sections[prefix] return _ChainMap(vardict, sectiondict, defaultdict, self._defaults) except KeyError: return _ChainMap(vardict, sectiondict, self._defaults)
[docs] def has_option(self, section, option): """Check for the existence of a given option in a given section. If the specified `section` is None or an empty string, DEFAULT is assumed. If the specified `section` does not exist, returns False.""" option = self.optionxform(option) if ((not section) or (section == self.default_section)): sect = self._defaults else: prefix = section.split(".", 1)[0] + ".DEFAULT" if ((prefix in self) and (option in self._sections[prefix])): return True if (section not in self._sections): return False else: sect = self._sections[section] return option in sect