diff options
author | Łukasz Langa <lukasz@langa.pl> | 2010-12-03 16:28:00 (GMT) |
---|---|---|
committer | Łukasz Langa <lukasz@langa.pl> | 2010-12-03 16:28:00 (GMT) |
commit | b6a6f5f886ed869612e16cd1e29a1190996dc78d (patch) | |
tree | 43e86bc6227ab409723a87c9d6ae8448c976f9da /Lib/configparser.py | |
parent | ecace28ef4486b3fee598e3854431d3a454e30f8 (diff) | |
download | cpython-b6a6f5f886ed869612e16cd1e29a1190996dc78d.zip cpython-b6a6f5f886ed869612e16cd1e29a1190996dc78d.tar.gz cpython-b6a6f5f886ed869612e16cd1e29a1190996dc78d.tar.bz2 |
Issue 10499: Modular interpolation in configparser
Diffstat (limited to 'Lib/configparser.py')
-rw-r--r-- | Lib/configparser.py | 540 |
1 files changed, 309 insertions, 231 deletions
diff --git a/Lib/configparser.py b/Lib/configparser.py index f9bb32c..56c02ba 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -4,23 +4,13 @@ A configuration file consists of sections, lead by a "[section]" header, and followed by "name: value" entries, with continuations and such in the style of RFC 822. -The option values can contain format strings which refer to other values in -the same section, or values in a 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. - Intrinsic defaults can be specified by passing them into the -ConfigParser constructor as a dictionary. +SafeConfigParser constructor as a dictionary. class: -ConfigParser -- responsible for parsing a list of - configuration files, and managing the parsed database. +SafeConfigParser -- responsible for parsing a list of + configuration files, and managing the parsed database. methods: @@ -316,7 +306,7 @@ class ParsingError(Error): def filename(self): """Deprecated, use `source'.""" warnings.warn( - "This 'filename' attribute will be removed in future versions. " + "The 'filename' attribute will be removed in future versions. " "Use 'source' instead.", DeprecationWarning, stacklevel=2 ) @@ -362,6 +352,204 @@ _COMPATIBLE = object() _UNSET = object() +class Interpolation: + """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 SafeConfigParser. + + 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 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): + if depth > MAX_INTERPOLATION_DEPTH: + raise InterpolationDepthError(option, section, rest) + 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 InterpolationMissingOptionError( + option, section, rest, 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): + if depth > MAX_INTERPOLATION_DEPTH: + raise InterpolationDepthError(option, section, rest) + 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 = parser.optionxform(m.group(1)).split(':') + rest = rest[m.end():] + sect = section + opt = option + try: + if len(path) == 1: + opt = path[0] + v = map[opt] + elif len(path) == 2: + sect = path[0] + opt = path[1] + v = parser.get(sect, opt, raw=True) + else: + raise InterpolationSyntaxError( + option, section, + "More than one ':' found: %r" % (rest,)) + except KeyError: + raise InterpolationMissingOptionError( + option, section, rest, var) + 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 BrokenInterpolation(Interpolation): + """Deprecated interpolation as implemented in the classic 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 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.""" @@ -388,7 +576,8 @@ class RawConfigParser(MutableMapping): # space/tab (?P<value>.*))?$ # everything up to eol """ - + # Interpolation algorithm to be used if the user does not specify another + _DEFAULT_INTERPOLATION = Interpolation() # Compiled regular expression for matching sections SECTCRE = re.compile(_SECT_TMPL, re.VERBOSE) # Compiled regular expression for matching options with typical separators @@ -406,7 +595,15 @@ class RawConfigParser(MutableMapping): allow_no_value=False, *, delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, strict=False, empty_lines_in_values=True, - default_section=DEFAULTSECT): + default_section=DEFAULTSECT, + interpolation=_UNSET): + + if self.__class__ is RawConfigParser: + warnings.warn( + "The RawConfigParser class will be removed in future versions." + " Use 'SafeConfigParser(interpolation=None)' instead.", + DeprecationWarning, stacklevel=2 + ) self._dict = dict_type self._sections = self._dict() self._defaults = self._dict() @@ -435,7 +632,11 @@ class RawConfigParser(MutableMapping): self._strict = strict self._allow_no_value = allow_no_value self._empty_lines_in_values = empty_lines_in_values - self._default_section=default_section + if interpolation is _UNSET: + self._interpolation = self._DEFAULT_INTERPOLATION + else: + self._interpolation = interpolation + self.default_section=default_section def defaults(self): return self._defaults @@ -451,7 +652,7 @@ class RawConfigParser(MutableMapping): Raise DuplicateSectionError if a section by the specified name already exists. Raise ValueError if name is DEFAULT. """ - if section == self._default_section: + if section == self.default_section: raise ValueError('Invalid section name: %s' % section) if section in self._sections: @@ -555,7 +756,7 @@ class RawConfigParser(MutableMapping): ) self.read_file(fp, source=filename) - def get(self, section, option, *, vars=None, fallback=_UNSET): + def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET): """Get an option value for a given section. If `vars' is provided, it must be a dictionary. The option is looked up @@ -563,7 +764,12 @@ class RawConfigParser(MutableMapping): If the key is not found and `fallback' is provided, it is used as a fallback value. `None' can be provided as a `fallback' value. - Arguments `vars' and `fallback' are keyword only. + If interpolation is enabled and the optional argument `raw' is False, + all interpolations are expanded in the return values. + + Arguments `raw', `vars', and `fallback' are keyword only. + + The section DEFAULT is special. """ try: d = self._unify_values(section, vars) @@ -574,61 +780,90 @@ class RawConfigParser(MutableMapping): return fallback option = self.optionxform(option) try: - return d[option] + value = d[option] except KeyError: if fallback is _UNSET: raise NoOptionError(option, section) else: return fallback - def items(self, section): - try: - d2 = self._sections[section] - except KeyError: - if section != self._default_section: - raise NoSectionError(section) - d2 = self._dict() - d = self._defaults.copy() - d.update(d2) - return d.items() + if raw or value is None: + return value + else: + return self._interpolation.before_get(self, section, option, value, + d) def _get(self, section, conv, option, **kwargs): return conv(self.get(section, option, **kwargs)) - def getint(self, section, option, *, vars=None, fallback=_UNSET): + def getint(self, section, option, *, raw=False, vars=None, + fallback=_UNSET): try: - return self._get(section, int, option, vars=vars) + return self._get(section, int, option, raw=raw, vars=vars) except (NoSectionError, NoOptionError): if fallback is _UNSET: raise else: return fallback - def getfloat(self, section, option, *, vars=None, fallback=_UNSET): + def getfloat(self, section, option, *, raw=False, vars=None, + fallback=_UNSET): try: - return self._get(section, float, option, vars=vars) + return self._get(section, float, option, raw=raw, vars=vars) except (NoSectionError, NoOptionError): if fallback is _UNSET: raise else: return fallback - def getboolean(self, section, option, *, vars=None, fallback=_UNSET): + def getboolean(self, section, option, *, raw=False, vars=None, + fallback=_UNSET): try: return self._get(section, self._convert_to_boolean, option, - vars=vars) + raw=raw, vars=vars) except (NoSectionError, NoOptionError): if fallback is _UNSET: raise else: return fallback + def items(self, section, raw=False, vars=None): + """Return a list of (name, value) tuples for each option in a section. + + All % interpolations are expanded in the return values, based on the + defaults passed into the constructor, unless the optional argument + `raw' is true. Additional substitutions may be provided using the + `vars' argument, which must be a dictionary whose contents overrides + any pre-existing defaults. + + The section DEFAULT is special. + """ + d = self._defaults.copy() + try: + d.update(self._sections[section]) + except KeyError: + if section != self.default_section: + raise NoSectionError(section) + # Update with the entry specific variables + if vars: + for key, value in vars.items(): + d[self.optionxform(key)] = value + options = list(d.keys()) + if raw: + return [(option, d[option]) + for option in options] + else: + return [(option, self._interpolation.before_get(self, section, + option, d[option], + d)) + for option in options] + def optionxform(self, optionstr): return optionstr.lower() def has_option(self, section, option): """Check for the existence of a given option in a given section.""" - if not section or section == self._default_section: + if not section or section == self.default_section: option = self.optionxform(option) return option in self._defaults elif section not in self._sections: @@ -640,7 +875,10 @@ class RawConfigParser(MutableMapping): def set(self, section, option, value=None): """Set an option.""" - if not section or section == self._default_section: + if value: + value = self._interpolation.before_set(self, section, option, + value) + if not section or section == self.default_section: sectdict = self._defaults else: try: @@ -660,7 +898,7 @@ class RawConfigParser(MutableMapping): else: d = self._delimiters[0] if self._defaults: - self._write_section(fp, self._default_section, + self._write_section(fp, self.default_section, self._defaults.items(), d) for section in self._sections: self._write_section(fp, section, @@ -670,6 +908,8 @@ class RawConfigParser(MutableMapping): """Write a single section to the specified `fp'.""" fp.write("[{}]\n".format(section_name)) for key, value in section_items: + value = self._interpolation.before_write(self, section_name, key, + value) if value is not None or not self._allow_no_value: value = delimiter + str(value).replace('\n', '\n\t') else: @@ -679,7 +919,7 @@ class RawConfigParser(MutableMapping): def remove_option(self, section, option): """Remove an option.""" - if not section or section == self._default_section: + if not section or section == self.default_section: sectdict = self._defaults else: try: @@ -701,7 +941,7 @@ class RawConfigParser(MutableMapping): return existed def __getitem__(self, key): - if key != self._default_section and not self.has_section(key): + if key != self.default_section and not self.has_section(key): raise KeyError(key) return self._proxies[key] @@ -715,21 +955,21 @@ class RawConfigParser(MutableMapping): self.read_dict({key: value}) def __delitem__(self, key): - if key == self._default_section: + if key == self.default_section: raise ValueError("Cannot remove the default section.") if not self.has_section(key): raise KeyError(key) self.remove_section(key) def __contains__(self, key): - return key == self._default_section or self.has_section(key) + return key == self.default_section or self.has_section(key) def __len__(self): return len(self._sections) + 1 # the default section def __iter__(self): # XXX does it break when underlying container state changed? - return itertools.chain((self._default_section,), self._sections.keys()) + return itertools.chain((self.default_section,), self._sections.keys()) def _read(self, fp, fpname): """Parse a sectioned configuration file. @@ -801,7 +1041,7 @@ class RawConfigParser(MutableMapping): lineno) cursect = self._sections[sectname] elements_added.add(sectname) - elif sectname == self._default_section: + elif sectname == self.default_section: cursect = self._defaults else: cursect = self._dict() @@ -836,7 +1076,7 @@ class RawConfigParser(MutableMapping): cursect[optname] = [optval] else: # valueless option handling - cursect[optname] = optval + cursect[optname] = None else: # a non-fatal parsing error occurred. set up the # exception but keep going. the exception will be @@ -849,12 +1089,16 @@ class RawConfigParser(MutableMapping): self._join_multiline_values() def _join_multiline_values(self): - all_sections = itertools.chain((self._defaults,), - self._sections.values()) - for options in all_sections: + defaults = self.default_section, self._defaults + all_sections = itertools.chain((defaults,), + self._sections.items()) + for section, options in all_sections: for name, val in options.items(): if isinstance(val, list): - options[name] = '\n'.join(val).rstrip() + val = '\n'.join(val).rstrip() + options[name] = self._interpolation.before_read(self, + section, + name, val) def _handle_error(self, exc, fpname, lineno, line): if not exc: @@ -871,7 +1115,7 @@ class RawConfigParser(MutableMapping): try: d.update(self._sections[section]) except KeyError: - if section != self._default_section: + if section != self.default_section: raise NoSectionError(section) # Update with the entry specific variables if vars: @@ -906,197 +1150,31 @@ class RawConfigParser(MutableMapping): raise TypeError("option values must be strings") - class ConfigParser(RawConfigParser): """ConfigParser implementing interpolation.""" - def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET): - """Get an option value for a given section. - - If `vars' is provided, it must be a dictionary. The option is looked up - in `vars' (if provided), `section', and in `DEFAULTSECT' in that order. - If the key is not found and `fallback' is provided, it is used as - a fallback value. `None' can be provided as a `fallback' value. - - All % interpolations are expanded in the return values, unless the - optional argument `raw' is true. Values for interpolation keys are - looked up in the same manner as the option. - - Arguments `raw', `vars', and `fallback' are keyword only. - - The section DEFAULT is special. - """ - try: - d = self._unify_values(section, vars) - except NoSectionError: - if fallback is _UNSET: - raise - else: - return fallback - option = self.optionxform(option) - try: - value = d[option] - except KeyError: - if fallback is _UNSET: - raise NoOptionError(option, section) - else: - return fallback - - if raw or value is None: - return value - else: - return self._interpolate(section, option, value, d) + _DEFAULT_INTERPOLATION = BrokenInterpolation() - def getint(self, section, option, *, raw=False, vars=None, - fallback=_UNSET): - try: - return self._get(section, int, option, raw=raw, vars=vars) - except (NoSectionError, NoOptionError): - if fallback is _UNSET: - raise - else: - return fallback - - def getfloat(self, section, option, *, raw=False, vars=None, - fallback=_UNSET): - try: - return self._get(section, float, option, raw=raw, vars=vars) - except (NoSectionError, NoOptionError): - if fallback is _UNSET: - raise - else: - return fallback - - def getboolean(self, section, option, *, raw=False, vars=None, - fallback=_UNSET): - try: - return self._get(section, self._convert_to_boolean, option, - raw=raw, vars=vars) - except (NoSectionError, NoOptionError): - if fallback is _UNSET: - raise - else: - return fallback - - def items(self, section, raw=False, vars=None): - """Return a list of (name, value) tuples for each option in a section. - - All % interpolations are expanded in the return values, based on the - defaults passed into the constructor, unless the optional argument - `raw' is true. Additional substitutions may be provided using the - `vars' argument, which must be a dictionary whose contents overrides - any pre-existing defaults. - - The section DEFAULT is special. - """ - d = self._defaults.copy() - try: - d.update(self._sections[section]) - except KeyError: - if section != self._default_section: - raise NoSectionError(section) - # Update with the entry specific variables - if vars: - for key, value in vars.items(): - d[self.optionxform(key)] = value - options = list(d.keys()) - if raw: - return [(option, d[option]) - for option in options] - else: - return [(option, self._interpolate(section, option, d[option], d)) - for option in options] - - def _interpolate(self, section, option, rawval, vars): - # do the string interpolation - value = rawval - depth = MAX_INTERPOLATION_DEPTH - while depth: # Loop through this until it's done - depth -= 1 - if value and "%(" in value: - value = self._KEYCRE.sub(self._interpolation_replace, value) - try: - value = value % vars - except KeyError as e: - raise InterpolationMissingOptionError( - option, section, rawval, e.args[0]) - else: - break - if value and "%(" in value: - raise InterpolationDepthError(option, section, rawval) - return value - - _KEYCRE = re.compile(r"%\(([^)]*)\)s|.") - - def _interpolation_replace(self, match): - s = match.group(1) - if s is None: - return match.group() - else: - return "%%(%s)s" % self.optionxform(s) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.__class__ is ConfigParser: + warnings.warn( + "The ConfigParser class will be removed in future versions." + " Use SafeConfigParser instead.", + DeprecationWarning, stacklevel=2 + ) class SafeConfigParser(ConfigParser): """ConfigParser implementing sane interpolation.""" - def _interpolate(self, section, option, rawval, vars): - # do the string interpolation - L = [] - self._interpolate_some(option, L, rawval, section, vars, 1) - return ''.join(L) - - _interpvar_re = re.compile(r"%\(([^)]+)\)s") - - def _interpolate_some(self, option, accum, rest, section, map, depth): - if depth > MAX_INTERPOLATION_DEPTH: - raise InterpolationDepthError(option, section, rest) - 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._interpvar_re.match(rest) - if m is None: - raise InterpolationSyntaxError(option, section, - "bad interpolation variable reference %r" % rest) - var = self.optionxform(m.group(1)) - rest = rest[m.end():] - try: - v = map[var] - except KeyError: - raise InterpolationMissingOptionError( - option, section, rest, var) - if "%" in v: - self._interpolate_some(option, accum, v, - section, map, depth + 1) - else: - accum.append(v) - else: - raise InterpolationSyntaxError( - option, section, - "'%%' must be followed by '%%' or '(', " - "found: %r" % (rest,)) + _DEFAULT_INTERPOLATION = BasicInterpolation() def set(self, section, option, value=None): - """Set an option. Extend ConfigParser.set: check for string values.""" + """Set an option. Extends RawConfigParser.set by validating type and + interpolation syntax on the value.""" self._validate_value_type(value) - # check for bad percent signs - if value: - tmp_value = value.replace('%%', '') # escaped percent signs - tmp_value = self._interpvar_re.sub('', tmp_value) # valid syntax - if '%' in tmp_value: - raise ValueError("invalid interpolation syntax in %r at " - "position %d" % (value, tmp_value.find('%'))) - ConfigParser.set(self, section, option, value) + super().set(section, option, value) class SectionProxy(MutableMapping): |