PKfJ,ґstdconfigparser.py# -*- coding: utf-8 -*- # Copyright (c) 2017 Wolfgang Langner # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. "A standard INI style configuration parser." from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals __version__ = "1.0.1" __all__ = ["NoSectionError", "DuplicateOptionError", "DuplicateSectionError", "NoOptionError", "InterpolationError", "InterpolationDepthError", "InterpolationMissingOptionError", "InterpolationSyntaxError", "ParsingError", "MissingSectionHeaderError", "ConfigParser", "SafeConfigParser", "RawConfigParser", "Interpolation", "BasicInterpolation", "ExtendedInterpolation", "LegacyInterpolation", "SectionProxy", "ConverterMapping", "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "StdConfigParser"] import sys PY2 = sys.version_info[0] == 2 PY33 = sys.version_info[:2] == (3, 3) PY34 = sys.version_info[:2] == (3, 4) def from_none(exc): """raise from_none(ValueError('a')) == raise ValueError('a') from None""" exc.__cause__ = None exc.__suppress_context__ = True return exc # whole Python 2 implementation based on the backport of configparser # lot of stuff copied also from Python standard library implementation if PY2: from collections import MutableMapping try: from collections import OrderedDict except ImportError: from ordereddict import OrderedDict from io import open try: from thread import get_ident except ImportError: try: from _thread import get_ident except ImportError: from _dummy_thread import get_ident # stuff from backports helpers str = type('str') # same as str = unicode because of __future__.unicode_literals # constants DEFAULTSECT = "DEFAULT" MAX_INTERPOLATION_DEPTH = 10 # Used in parser getters to indicate the default behaviour when a specific # option is not found it to raise an exception. Created to enable `None' as # a valid fallback value. _UNSET = object() # from reprlib 3.2.1 def recursive_repr(fillvalue='...'): 'Decorator to make a repr function return fillvalue for a recursive call' def decorating_function(user_function): repr_running = set() def wrapper(self): key = id(self), get_ident() if key in repr_running: return fillvalue repr_running.add(key) try: result = user_function(self) finally: repr_running.discard(key) return result # Can't use functools.wraps() here because of bootstrap issues wrapper.__module__ = getattr(user_function, '__module__') wrapper.__doc__ = getattr(user_function, '__doc__') wrapper.__name__ = getattr(user_function, '__name__') wrapper.__annotations__ = getattr(user_function, '__annotations__', {}) return wrapper return decorating_function # from collections 3.2.1 class ChainMap(MutableMapping): ''' A ChainMap groups multiple dicts (or other mappings) together to create a single, updateable view. The underlying mappings are stored in a list. That list is public and can accessed or updated using the *maps* attribute. There is no other state. Lookups search the underlying mappings successively until a key is found. In contrast, writes, updates, and deletions only operate on the first mapping. ''' def __init__(self, *maps): '''Initialize a ChainMap by setting *maps* to the given mappings. If no mappings are provided, a single empty dictionary is used. ''' self.maps = list(maps) or [{}] # always at least one map def __missing__(self, key): raise KeyError(key) def __getitem__(self, key): for mapping in self.maps: try: return mapping[key] # can't use 'key in mapping' with defaultdict except KeyError: pass return self.__missing__(key) # support subclasses that define __missing__ def get(self, key, default=None): return self[key] if key in self else default def __len__(self): return len(set().union(*self.maps)) # reuses stored hash values if possible def __iter__(self): return iter(set().union(*self.maps)) def __contains__(self, key): return any(key in m for m in self.maps) @recursive_repr() def __repr__(self): return '{0.__class__.__name__}({1})'.format( self, ', '.join(map(repr, self.maps))) @classmethod def fromkeys(cls, iterable, *args): 'Create a ChainMap with a single dict created from the iterable.' return cls(dict.fromkeys(iterable, *args)) def copy(self): 'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]' return self.__class__(self.maps[0].copy(), *self.maps[1:]) __copy__ = copy def new_child(self): # like Django's Context.push() 'New ChainMap with a new dict followed by all previous maps.' return self.__class__({}, *self.maps) @property def parents(self): # like Django's Context.pop() 'New ChainMap from maps[1:].' return self.__class__(*self.maps[1:]) def __setitem__(self, key, value): self.maps[0][key] = value def __delitem__(self, key): try: del self.maps[0][key] except KeyError: raise KeyError('Key not found in the first mapping: {!r}'.format(key)) def popitem(self): 'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.' try: return self.maps[0].popitem() except KeyError: raise KeyError('No keys found in the first mapping.') def pop(self, key, *args): 'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].' try: return self.maps[0].pop(key, *args) except KeyError: raise KeyError('Key not found in the first mapping: {!r}'.format(key)) def clear(self): 'Clear maps[0], leaving maps[1:] intact.' self.maps[0].clear() if PY33 or PY34: from reprlib import recursive_repr from collections import ChainMap, MutableMapping, OrderedDict # noqa from configparser import _UNSET, DEFAULTSECT, MAX_INTERPOLATION_DEPTH if PY2 or PY33 or PY34: import functools from io import StringIO import itertools import re import warnings # exception classes class Error(Exception): """Base class for ConfigParser exceptions.""" def __init__(self, msg=''): self.message = msg Exception.__init__(self, msg) def __repr__(self): return self.message __str__ = __repr__ class NoSectionError(Error): """Raised when no section matches a requested option.""" def __init__(self, section): Error.__init__(self, 'No section: %r' % (section,)) self.section = section self.args = (section, ) class DuplicateSectionError(Error): """Raised when a section is repeated in an input source. Possible repetitions that raise this exception are: multiple creation using the API or in strict parsers when a section is found more than once in a single input file, string or dictionary. """ def __init__(self, section, source=None, lineno=None): msg = [repr(section), " already exists"] if source is not None: message = ["While reading from ", repr(source)] if lineno is not None: message.append(" [line {0:2d}]".format(lineno)) message.append(": section ") message.extend(msg) msg = message else: msg.insert(0, "Section ") Error.__init__(self, "".join(msg)) self.section = section self.source = source self.lineno = lineno self.args = (section, source, lineno) class DuplicateOptionError(Error): """Raised by strict parsers when an option is repeated in an input source. Current implementation raises this exception only when an option is found more than once in a single file, string or dictionary. """ def __init__(self, section, option, source=None, lineno=None): msg = [repr(option), " in section ", repr(section), " already exists"] if source is not None: message = ["While reading from ", repr(source)] if lineno is not None: message.append(" [line {0:2d}]".format(lineno)) message.append(": option ") message.extend(msg) msg = message else: msg.insert(0, "Option ") Error.__init__(self, "".join(msg)) self.section = section self.option = option self.source = source self.lineno = lineno self.args = (section, option, source, lineno) class NoOptionError(Error): """A requested option was not found.""" def __init__(self, option, section): Error.__init__(self, "No option %r in section: %r" % (option, section)) self.option = option self.section = section self.args = (option, section) class InterpolationError(Error): """Base class for interpolation-related exceptions.""" def __init__(self, option, section, msg): Error.__init__(self, msg) self.option = option self.section = section self.args = (option, section, msg) class InterpolationMissingOptionError(InterpolationError): """A string substitution required a setting which was not available.""" def __init__(self, option, section, rawval, reference): msg = ("Bad value substitution: option {0!r} in section {1!r} contains " "an interpolation key {2!r} which is not a valid option name. " "Raw value: {3!r}".format(option, section, reference, rawval)) InterpolationError.__init__(self, option, section, msg) self.reference = reference self.args = (option, section, rawval, reference) class InterpolationSyntaxError(InterpolationError): """Raised when the source text contains invalid syntax. Current implementation raises this exception when the source text into which substitutions are made does not conform to the required syntax. """ class InterpolationDepthError(InterpolationError): """Raised when substitutions are nested too deeply.""" def __init__(self, option, section, rawval): msg = ("Recursion limit exceeded in value substitution: option {0!r} " "in section {1!r} contains an interpolation key which " "cannot be substituted in {2} steps. Raw value: {3!r}" "".format(option, section, MAX_INTERPOLATION_DEPTH, rawval)) InterpolationError.__init__(self, option, section, msg) self.args = (option, section, rawval) class ParsingError(Error): """Raised when a configuration file does not follow legal syntax.""" def __init__(self, source=None, filename=None): # Exactly one of `source'/`filename' arguments has to be given. # `filename' kept for compatibility. if filename and source: raise ValueError("Cannot specify both `filename' and `source'. " "Use `source'.") elif not filename and not source: raise ValueError("Required argument `source' not given.") elif filename: source = filename Error.__init__(self, 'Source contains parsing errors: %r' % source) self.source = source self.errors = [] self.args = (source, ) @property def filename(self): """Deprecated, use `source'.""" warnings.warn( "The 'filename' attribute will be removed in future versions. " "Use 'source' instead.", DeprecationWarning, stacklevel=2 ) return self.source @filename.setter def filename(self, value): """Deprecated, user `source'.""" warnings.warn( "The 'filename' attribute will be removed in future versions. " "Use 'source' instead.", DeprecationWarning, stacklevel=2 ) self.source = value def append(self, lineno, line): self.errors.append((lineno, line)) self.message += '\n\t[line %2d]: %s' % (lineno, line) class MissingSectionHeaderError(ParsingError): """Raised when a key-value pair is found before any section header.""" def __init__(self, filename, lineno, line): Error.__init__( self, 'File contains no section headers.\nfile: %r, line: %d\n%r' % (filename, lineno, line)) self.source = filename self.lineno = lineno self.line = line self.args = (filename, lineno, line) class Interpolation(object): """Dummy interpolation that passes the value through with no changes.""" def before_get(self, parser, section, option, value, defaults): return value def before_set(self, parser, section, option, value): return value def before_read(self, parser, section, option, value): return value def before_write(self, parser, section, option, value): return value class BasicInterpolation(Interpolation): """Interpolation as implemented in the classic ConfigParser. The option values can contain format strings which refer to other values in the same section, or values in the special default section. For example: something: %(dir)s/whatever would resolve the "%(dir)s" to the value of dir. All reference expansions are done late, on demand. If a user needs to use a bare % in a configuration file, she can escape it by writing %%. Other % usage is considered a user error and raises `InterpolationSyntaxError'.""" _KEYCRE = re.compile(r"%\(([^)]+)\)s") def before_get(self, parser, section, option, value, defaults): L = [] self._interpolate_some(parser, option, L, value, section, defaults, 1) return ''.join(L) def before_set(self, parser, section, option, value): tmp_value = value.replace('%%', '') # escaped percent signs tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax if '%' in tmp_value: raise ValueError("invalid interpolation syntax in %r at " "position %d" % (value, tmp_value.find('%'))) return value def _interpolate_some(self, parser, option, accum, rest, section, map, depth): rawval = parser.get(section, option, raw=True, fallback=rest) if depth > MAX_INTERPOLATION_DEPTH: raise InterpolationDepthError(option, section, rawval) while rest: p = rest.find("%") if p < 0: accum.append(rest) return if p > 0: accum.append(rest[:p]) rest = rest[p:] # p is no longer used c = rest[1:2] if c == "%": accum.append("%") rest = rest[2:] elif c == "(": m = self._KEYCRE.match(rest) if m is None: raise InterpolationSyntaxError(option, section, "bad interpolation variable reference %r" % rest) var = parser.optionxform(m.group(1)) rest = rest[m.end():] try: v = map[var] except KeyError: raise from_none(InterpolationMissingOptionError( option, section, rawval, var)) if "%" in v: self._interpolate_some(parser, option, accum, v, section, map, depth + 1) else: accum.append(v) else: raise InterpolationSyntaxError( option, section, "'%%' must be followed by '%%' or '(', " "found: %r" % (rest,)) class ExtendedInterpolation(Interpolation): """Advanced variant of interpolation, supports the syntax used by `zc.buildout'. Enables interpolation between sections.""" _KEYCRE = re.compile(r"\$\{([^}]+)\}") def before_get(self, parser, section, option, value, defaults): L = [] self._interpolate_some(parser, option, L, value, section, defaults, 1) return ''.join(L) 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 %r at " "position %d" % (value, tmp_value.find('$'))) return value def _interpolate_some(self, parser, option, accum, rest, section, map, depth): rawval = parser.get(section, option, raw=True, fallback=rest) if depth > MAX_INTERPOLATION_DEPTH: raise InterpolationDepthError(option, section, rawval) while rest: p = rest.find("$") if p < 0: accum.append(rest) return if p > 0: accum.append(rest[:p]) rest = rest[p:] # p is no longer used c = rest[1:2] if c == "$": accum.append("$") rest = rest[2:] elif c == "{": m = self._KEYCRE.match(rest) if m is None: raise InterpolationSyntaxError(option, section, "bad interpolation variable reference %r" % rest) path = m.group(1).split(':') rest = rest[m.end():] sect = section opt = option try: if len(path) == 1: opt = parser.optionxform(path[0]) v = map[opt] elif len(path) == 2: sect = path[0] opt = parser.optionxform(path[1]) v = parser.get(sect, opt, raw=True) else: raise InterpolationSyntaxError( option, section, "More than one ':' found: %r" % (rest,)) except (KeyError, NoSectionError, NoOptionError): raise from_none(InterpolationMissingOptionError( option, section, rawval, ":".join(path))) if "$" in v: self._interpolate_some(parser, opt, accum, v, sect, dict(parser.items(sect, raw=True)), depth + 1) else: accum.append(v) else: raise InterpolationSyntaxError( option, section, "'$' must be followed by '$' or '{', " "found: %r" % (rest,)) class LegacyInterpolation(Interpolation): """Deprecated interpolation used in old versions of ConfigParser. Use BasicInterpolation or ExtendedInterpolation instead.""" _KEYCRE = re.compile(r"%\(([^)]*)\)s|.") def before_get(self, parser, section, option, value, vars): rawval = value depth = MAX_INTERPOLATION_DEPTH while depth: # Loop through this until it's done depth -= 1 if value and "%(" in value: replace = functools.partial(self._interpolation_replace, parser=parser) value = self._KEYCRE.sub(replace, value) try: value = value % vars except KeyError as e: raise from_none(InterpolationMissingOptionError( option, section, rawval, e.args[0])) else: break if value and "%(" in value: raise InterpolationDepthError(option, section, rawval) return value def before_set(self, parser, section, option, value): return value @staticmethod def _interpolation_replace(match, parser): s = match.group(1) if s is None: return match.group() else: return "%%(%s)s" % parser.optionxform(s) class RawConfigParser(MutableMapping): """ConfigParser that does not do interpolation.""" # Regular expressions for parsing section headers and options _SECT_TMPL = r""" \[ # [ (?P
[^]]+) # very permissive! \] # ] """ _OPT_TMPL = r""" (?P