Index: Lib/configparser.py =================================================================== --- Lib/configparser.py (revision 86668) +++ Lib/configparser.py (working copy) @@ -316,7 +316,7 @@ 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 +362,101 @@ _UNSET = object() +class ClassicInterpolation: + """Interpolation as implemented in the classic SafeConfigParser.""" + + _interpvar_re = re.compile(r"%\(([^)]+)\)s") + + def process(self, parser, section, option, rawval, vars): + L = [] + self._interpolate_some(parser, option, L, rawval, section, vars, 1) + return ''.join(L) + + def validate(self, parser, section, option, 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('%'))) + + 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._interpvar_re.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 BrokenInterpolation: + """Deprecated interpolation as implemented in the classic ConfigParser.""" + + _KEYCRE = re.compile(r"%\(([^)]*)\)s|.") + + def process(self, parser, section, option, rawval, vars): + value = rawval + 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 validate(self, parser, section, option, value): + pass + + @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 +483,8 @@ # space/tab (?P.*))?$ # everything up to eol """ - + # Value interpolation algorithm to use if the user does not specify another + _DEFAULT_INTERPOLATION = lambda self: None # Compiled regular expression for matching sections SECTCRE = re.compile(_SECT_TMPL, re.VERBOSE) # Compiled regular expression for matching options with typical separators @@ -406,7 +502,15 @@ 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,6 +539,10 @@ self._strict = strict self._allow_no_value = allow_no_value self._empty_lines_in_values = empty_lines_in_values + if interpolation is _UNSET: + self._interpolation = self._DEFAULT_INTERPOLATION() + else: + self._interpolation = interpolation() self._default_section=default_section def defaults(self): @@ -555,7 +663,7 @@ ) 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 +671,12 @@ 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,55 +687,82 @@ 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 self._interpolation is None or value is None: + return value + else: + return self._interpolation.process(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 or self._interpolation is None: + return [(option, d[option]) + for option in options] + else: + return [(option, self._interpolation.process(self, section, option, + d[option], d)) + for option in options] + def optionxform(self, optionstr): return optionstr.lower() @@ -906,197 +1046,33 @@ 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. + _DEFAULT_INTERPOLATION = BrokenInterpolation - 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. + 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 + ) - 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) - - 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) - - 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) + _DEFAULT_INTERPOLATION = ClassicInterpolation - _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,)) - 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) + if value and self._interpolation is not None: + self._interpolation.validate(self, section, option, value) + super().set(section, option, value) class SectionProxy(MutableMapping): Index: Lib/test/test_cfgparser.py =================================================================== --- Lib/test/test_cfgparser.py (revision 86668) +++ Lib/test/test_cfgparser.py (working copy) @@ -4,6 +4,7 @@ import os import unittest import textwrap +import warnings from test import support @@ -44,7 +45,10 @@ strict=self.strict, default_section=self.default_section, ) - return self.config_class(**arguments) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + instance = self.config_class(**arguments) + return instance def fromstring(self, string, defaults=None): cf = self.newconfig(defaults) @@ -910,7 +914,9 @@ def prepare(self, config_class): # This is the default, but that's the point. - cp = config_class(allow_no_value=False) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + cp = config_class(allow_no_value=False) cp.add_section("section") cp.set("section", "option", None) sio = io.StringIO()