diff options
Diffstat (limited to 'Lib/configparser.py')
| -rw-r--r-- | Lib/configparser.py | 1289 | 
1 files changed, 898 insertions, 391 deletions
| diff --git a/Lib/configparser.py b/Lib/configparser.py index c7ae270759..12ba5ad81c 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -1,102 +1,134 @@  """Configuration file parser. -A setup file consists of sections, lead by a "[section]" header, +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.  class:  ConfigParser -- responsible for parsing a list of -                configuration files, and managing the parsed database. +                    configuration files, and managing the parsed database.      methods: -    __init__(defaults=None) -        create the parser and specify a dictionary of intrinsic defaults.  The -        keys must be strings, the values must be appropriate for %()s string -        interpolation.  Note that `__name__' is always an intrinsic default; -        its value is the section's name. +    __init__(defaults=None, dict_type=_default_dict, allow_no_value=False, +             delimiters=('=', ':'), comment_prefixes=('#', ';'), +             inline_comment_prefixes=None, strict=True, +             empty_lines_in_values=True): +        Create the parser. When `defaults' is given, it is initialized into the +        dictionary or intrinsic defaults. The keys must be strings, the values +        must be appropriate for %()s string interpolation. + +        When `dict_type' is given, it will be used to create the dictionary +        objects for the list of sections, for the options within a section, and +        for the default values. + +        When `delimiters' is given, it will be used as the set of substrings +        that divide keys from values. + +        When `comment_prefixes' is given, it will be used as the set of +        substrings that prefix comments in empty lines. Comments can be +        indented. + +        When `inline_comment_prefixes' is given, it will be used as the set of +        substrings that prefix comments in non-empty lines. + +        When `strict` is True, the parser won't allow for any section or option +        duplicates while reading from a single source (file, string or +        dictionary). Default is True. + +        When `empty_lines_in_values' is False (default: True), each empty line +        marks the end of an option. Otherwise, internal empty lines of +        a multiline option are kept as part of the value. + +        When `allow_no_value' is True (default: False), options without +        values are accepted; the value presented for these is None.      sections() -        return all the configuration section names, sans DEFAULT +        Return all the configuration section names, sans DEFAULT.      has_section(section) -        return whether the given section exists +        Return whether the given section exists.      has_option(section, option) -        return whether the given option exists in the given section +        Return whether the given option exists in the given section.      options(section) -        return list of configuration options for the named section +        Return list of configuration options for the named section. -    read(filenames) -        read and parse the list of named configuration files, given by +    read(filenames, encoding=None) +        Read and parse the list of named configuration files, given by          name.  A single filename is also allowed.  Non-existing files          are ignored.  Return list of successfully read files. -    readfp(fp, filename=None) -        read and parse one configuration file, given as a file object. -        The filename defaults to fp.name; it is only used in error -        messages (if fp has no `name' attribute, the string `<???>' is used). +    read_file(f, filename=None) +        Read and parse one configuration file, given as a file object. +        The filename defaults to f.name; it is only used in error +        messages (if f has no `name' attribute, the string `<???>' is used). + +    read_string(string) +        Read configuration from a given string. -    get(section, option, raw=False, vars=None) -        return a string value for the named option.  All % interpolations are +    read_dict(dictionary) +        Read configuration from a dictionary. Keys are section names, +        values are dictionaries with keys and values that should be present +        in the section. If the used dictionary type preserves order, sections +        and their keys will be added in order. Values are automatically +        converted to strings. + +    get(section, option, raw=False, vars=None, fallback=_UNSET) +        Return a string value for the named option.  All % interpolations are          expanded in the return values, based on the defaults passed into the          constructor and the DEFAULT section.  Additional substitutions may be          provided using the `vars' argument, which must be a dictionary whose -        contents override any pre-existing defaults. +        contents override any pre-existing defaults. If `option' is a key in +        `vars', the value from `vars' is used. -    getint(section, options) -        like get(), but convert value to an integer +    getint(section, options, raw=False, vars=None, fallback=_UNSET) +        Like get(), but convert value to an integer. -    getfloat(section, options) -        like get(), but convert value to a float +    getfloat(section, options, raw=False, vars=None, fallback=_UNSET) +        Like get(), but convert value to a float. -    getboolean(section, options) -        like get(), but convert value to a boolean (currently case +    getboolean(section, options, raw=False, vars=None, fallback=_UNSET) +        Like get(), but convert value to a boolean (currently case          insensitively defined as 0, false, no, off for False, and 1, true,          yes, on for True).  Returns False or True. -    items(section, raw=False, vars=None) +    items(section=_UNSET, raw=False, vars=None) +        If section is given, return a list of tuples with (section_name, +        section_proxy) for each section, including DEFAULTSECT. Otherwise,          return a list of tuples with (name, value) for each option          in the section.      remove_section(section) -        remove the given file section and all its options +        Remove the given file section and all its options.      remove_option(section, option) -        remove the given option from the given section +        Remove the given option from the given section.      set(section, option, value) -        set the given option +        Set the given option. -    write(fp) -        write the configuration state in .ini format +    write(fp, space_around_delimiters=True) +        Write the configuration state in .ini format. If +        `space_around_delimiters' is True (the default), delimiters +        between keys and values are surrounded by spaces.  """ -try: -    from collections import OrderedDict as _default_dict -except ImportError: -    # fallback for setup.py which hasn't yet built _collections -    _default_dict = dict - +from collections import MutableMapping, OrderedDict as _default_dict, _ChainMap +import functools +import io +import itertools  import re +import sys +import warnings -__all__ = ["NoSectionError", "DuplicateSectionError", "NoOptionError", -           "InterpolationError", "InterpolationDepthError", +__all__ = ["NoSectionError", "DuplicateOptionError", "DuplicateSectionError", +           "NoOptionError", "InterpolationError", "InterpolationDepthError",             "InterpolationSyntaxError", "ParsingError",             "MissingSectionHeaderError",             "ConfigParser", "SafeConfigParser", "RawConfigParser", @@ -114,12 +146,14 @@ class Error(Exception):      def _get_message(self):          """Getter for 'message'; needed only to override deprecation in -        BaseException.""" +        BaseException. +        """          return self.__message      def _set_message(self, value):          """Setter for 'message'; needed only to override deprecation in -        BaseException.""" +        BaseException. +        """          self.__message = value      # BaseException.message has been deprecated since Python 2.6.  To prevent @@ -136,19 +170,68 @@ class Error(Exception):      __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 multiply-created.""" +    """Raised when a section is repeated in an input source. -    def __init__(self, section): -        Error.__init__(self, "Section %r already exists" % section) +    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 ", 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 ", 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.""" @@ -158,6 +241,8 @@ class NoOptionError(Error):                         (option, section))          self.option = option          self.section = section +        self.args = (option, section) +  class InterpolationError(Error):      """Base class for interpolation-related exceptions.""" @@ -166,6 +251,8 @@ class InterpolationError(Error):          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.""" @@ -179,10 +266,16 @@ class InterpolationMissingOptionError(InterpolationError):                 % (section, option, 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 into which substitutions are made -    does not conform to the required syntax.""" +    """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.""" @@ -194,19 +287,52 @@ class InterpolationDepthError(InterpolationError):                 "\trawval : %s\n"                 % (section, option, 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, filename): -        Error.__init__(self, 'File contains parsing errors: %s' % filename) -        self.filename = filename +    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: %s' % 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.""" @@ -215,19 +341,294 @@ class MissingSectionHeaderError(ParsingError):              self,              'File contains no section headers.\nfile: %s, line: %d\n%r' %              (filename, lineno, line)) -        self.filename = filename +        self.source = filename          self.lineno = lineno          self.line = line +        self.args = (filename, lineno, line) + + +# 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() + + +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 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 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 = 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 InterpolationMissingOptionError( +                        option, section, rest, ":".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 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<header>[^]]+)                  # very permissive! +        \]                                 # ] +        """ +    _OPT_TMPL = r""" +        (?P<option>.*?)                    # very permissive! +        \s*(?P<vi>{delim})\s*              # any number of space/tab, +                                           # followed by any of the +                                           # allowed delimiters, +                                           # followed by any space/tab +        (?P<value>.*)$                     # everything up to eol +        """ +    _OPT_NV_TMPL = r""" +        (?P<option>.*?)                    # very permissive! +        \s*(?:                             # any number of space/tab, +        (?P<vi>{delim})\s*                 # optionally followed by +                                           # any of the allowed +                                           # delimiters, followed by any +                                           # 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 +    OPTCRE = re.compile(_OPT_TMPL.format(delim="=|:"), re.VERBOSE) +    # Compiled regular expression for matching options with optional values +    # delimited using typical separators +    OPTCRE_NV = re.compile(_OPT_NV_TMPL.format(delim="=|:"), re.VERBOSE) +    # Compiled regular expression for matching leading whitespace in a line +    NONSPACECRE = re.compile(r"\S") +    # Possible boolean values in the configuration. +    BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True, +                      '0': False, 'no': False, 'false': False, 'off': False} + +    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): -class RawConfigParser: -    def __init__(self, defaults=None, dict_type=_default_dict):          self._dict = dict_type          self._sections = self._dict()          self._defaults = self._dict() +        self._proxies = self._dict() +        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) +        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._interpolation = interpolation +        if self._interpolation is _UNSET: +            self._interpolation = self._DEFAULT_INTERPOLATION +        if self._interpolation is None: +            self._interpolation = Interpolation()      def defaults(self):          return self._defaults @@ -241,15 +642,15 @@ class RawConfigParser:          """Create a new section in the configuration.          Raise DuplicateSectionError if a section by the specified name -        already exists. Raise ValueError if name is DEFAULT or any of it's -        case-insensitive variants. +        already exists. Raise ValueError if name is DEFAULT.          """ -        if section.lower() == "default": -            raise ValueError('Invalid section name: %s' % section) +        if section == self.default_section: +            raise ValueError('Invalid section name: %r' % section)          if section in self._sections:              raise DuplicateSectionError(section)          self._sections[section] = self._dict() +        self._proxies[section] = SectionProxy(self, section)      def has_section(self, section):          """Indicate whether the named section is present in the configuration. @@ -265,11 +666,9 @@ class RawConfigParser:          except KeyError:              raise NoSectionError(section)          opts.update(self._defaults) -        if '__name__' in opts: -            del opts['__name__']          return list(opts.keys()) -    def read(self, filenames): +    def read(self, filenames, encoding=None):          """Read and parse a filename or a list of filenames.          Files that cannot be opened are silently ignored; this is @@ -286,83 +685,181 @@ class RawConfigParser:          read_ok = []          for filename in filenames:              try: -                fp = open(filename) +                with open(filename, encoding=encoding) as fp: +                    self._read(fp, filename)              except IOError:                  continue -            self._read(fp, filename) -            fp.close()              read_ok.append(filename)          return read_ok -    def readfp(self, fp, filename=None): +    def read_file(self, f, source=None):          """Like read() but the argument must be a file-like object. -        The `fp' argument must have a `readline' method.  Optional -        second argument is the `filename', which if not given, is -        taken from fp.name.  If fp has no `name' attribute, `<???>' is -        used. - +        The `f' argument must be iterable, returning one line at a time. +        Optional second argument is the `source' specifying the name of the +        file being read. If not given, it is taken from f.name. If `f' has no +        `name' attribute, `<???>' is used.          """ -        if filename is None: +        if source is None:              try: -                filename = fp.name +                source = f.name              except AttributeError: -                filename = '<???>' -        self._read(fp, filename) +                source = '<???>' +        self._read(f, source) -    def get(self, section, option): -        opt = self.optionxform(option) -        if section not in self._sections: -            if section != DEFAULTSECT: -                raise NoSectionError(section) -            if opt in self._defaults: -                return self._defaults[opt] +    def read_string(self, string, source='<string>'): +        """Read configuration from a given string.""" +        sfile = io.StringIO(string) +        self.read_file(sfile, source) + +    def read_dict(self, dictionary, source='<dict>'): +        """Read configuration from a dictionary. + +        Keys are section names, values are dictionaries with keys and values +        that should be present in the section. If the used dictionary type +        preserves order, sections and their keys will be added in order. + +        All types held in the dictionary are converted to strings during +        reading, including section names, option names and keys. + +        Optional second argument is the `source' specifying the name of the +        dictionary being read. +        """ +        elements_added = set() +        for section, keys in dictionary.items(): +            section = str(section) +            try: +                self.add_section(section) +            except (DuplicateSectionError, ValueError): +                if self._strict and section in elements_added: +                    raise +            elements_added.add(section) +            for key, value in keys.items(): +                key = self.optionxform(str(key)) +                if value is not None: +                    value = str(value) +                if self._strict and (section, key) in elements_added: +                    raise DuplicateOptionError(section, key, source) +                elements_added.add((section, key)) +                self.set(section, key, value) + +    def readfp(self, fp, filename=None): +        """Deprecated, use read_file instead.""" +        warnings.warn( +            "This method will be removed in future versions.  " +            "Use 'parser.read_file()' instead.", +            DeprecationWarning, stacklevel=2 +        ) +        self.read_file(fp, source=filename) + +    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. + +        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) +        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) -        elif opt in self._sections[section]: -            return self._sections[section][opt] -        elif opt in self._defaults: -            return self._defaults[opt] +            else: +                return fallback + +        if raw or value is None: +            return value          else: -            raise NoOptionError(option, section) +            return self._interpolation.before_get(self, section, option, value, +                                                  d) -    def items(self, section): +    def _get(self, section, conv, option, **kwargs): +        return conv(self.get(section, option, **kwargs)) + +    def getint(self, section, option, *, raw=False, vars=None, +               fallback=_UNSET):          try: -            d2 = self._sections[section] -        except KeyError: -            if section != DEFAULTSECT: -                raise NoSectionError(section) -            d2 = self._dict() -        d = self._defaults.copy() -        d.update(d2) -        if "__name__" in d: -            del d["__name__"] -        return d.items() +            return self._get(section, int, option, raw=raw, vars=vars) +        except (NoSectionError, NoOptionError): +            if fallback is _UNSET: +                raise +            else: +                return fallback -    def _get(self, section, conv, option): -        return conv(self.get(section, option)) +    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 getint(self, section, option): -        return self._get(section, int, option) +    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 getfloat(self, section, option): -        return self._get(section, float, option) +    def items(self, section=_UNSET, raw=False, vars=None): +        """Return a list of (name, value) tuples for each option in a section. -    _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, -                       '0': False, 'no': False, 'false': False, 'off': False} +        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. -    def getboolean(self, section, option): -        v = self.get(section, option) -        if v.lower() not in self._boolean_states: -            raise ValueError('Not a boolean: %s' % v) -        return self._boolean_states[v.lower()] +        The section DEFAULT is special. +        """ +        if section is _UNSET: +            return super().items() +        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 +        value_getter = lambda option: self._interpolation.before_get(self, +            section, option, d[option], d) +        if raw: +            value_getter = lambda option: d[option] +        return [(option, value_getter(option)) for option in d.keys()]      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 == DEFAULTSECT: +        """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.""" +        if not section or section == self.default_section:              option = self.optionxform(option)              return option in self._defaults          elif section not in self._sections: @@ -372,9 +869,12 @@ class RawConfigParser:              return (option in self._sections[section]                      or option in self._defaults) -    def set(self, section, option, value): +    def set(self, section, option, value=None):          """Set an option.""" -        if not section or section == DEFAULTSECT: +        if value: +            value = self._interpolation.before_set(self, section, option, +                                                   value) +        if not section or section == self.default_section:              sectdict = self._defaults          else:              try: @@ -383,24 +883,39 @@ class RawConfigParser:                  raise NoSectionError(section)          sectdict[self.optionxform(option)] = value -    def write(self, fp): -        """Write an .ini-format representation of the configuration state.""" +    def write(self, fp, space_around_delimiters=True): +        """Write an .ini-format representation of the configuration state. + +        If `space_around_delimiters' is True (the default), delimiters +        between keys and values are surrounded by spaces. +        """ +        if space_around_delimiters: +            d = " {} ".format(self._delimiters[0]) +        else: +            d = self._delimiters[0]          if self._defaults: -            fp.write("[%s]\n" % DEFAULTSECT) -            for (key, value) in self._defaults.items(): -                fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t'))) -            fp.write("\n") +            self._write_section(fp, self.default_section, +                                    self._defaults.items(), d)          for section in self._sections: -            fp.write("[%s]\n" % section) -            for (key, value) in self._sections[section].items(): -                if key != "__name__": -                    fp.write("%s = %s\n" % -                             (key, str(value).replace('\n', '\n\t'))) -            fp.write("\n") +            self._write_section(fp, section, +                                self._sections[section].items(), d) + +    def _write_section(self, fp, section_name, section_items, delimiter): +        """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: +                value = "" +            fp.write("{}{}\n".format(key, value)) +        fp.write("\n")      def remove_option(self, section, option):          """Remove an option.""" -        if not section or section == DEFAULTSECT: +        if not section or section == self.default_section:              sectdict = self._defaults          else:              try: @@ -418,69 +933,117 @@ class RawConfigParser:          existed = section in self._sections          if existed:              del self._sections[section] +            del self._proxies[section]          return existed -    # -    # Regular expressions for parsing section headers and options. -    # -    SECTCRE = re.compile( -        r'\['                                 # [ -        r'(?P<header>[^]]+)'                  # very permissive! -        r'\]'                                 # ] -        ) -    OPTCRE = re.compile( -        r'(?P<option>[^:=\s][^:=]*)'          # very permissive! -        r'\s*(?P<vi>[:=])\s*'                 # any number of space/tab, -                                              # followed by separator -                                              # (either : or =), followed -                                              # by any # space/tab -        r'(?P<value>.*)$'                     # everything up to eol -        ) +    def __getitem__(self, key): +        if key != self.default_section and not self.has_section(key): +            raise KeyError(key) +        return self._proxies[key] + +    def __setitem__(self, key, value): +        # To conform with the mapping protocol, overwrites existing values in +        # the section. + +        # XXX this is not atomic if read_dict fails at any point. Then again, +        # no update method in configparser is atomic in this implementation. +        self.remove_section(key) +        self.read_dict({key: value}) + +    def __delitem__(self, key): +        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) + +    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())      def _read(self, fp, fpname): -        """Parse a sectioned setup file. - -        The sections in setup file contains a title line at the top, -        indicated by a name in square brackets (`[]'), plus key/value -        options lines, indicated by `name: value' format lines. -        Continuations are represented by an embedded newline then -        leading whitespace.  Blank lines, lines beginning with a '#', -        and just about everything else are ignored. +        """Parse a sectioned configuration file. + +        Each section in a configuration file contains a header, indicated by +        a name in square brackets (`[]'), plus key/value options, indicated by +        `name' and `value' delimited with a specific substring (`=' or `:' by +        default). + +        Values can span multiple lines, as long as they are indented deeper +        than the first line of the value. Depending on the parser's mode, blank +        lines may be treated as parts of multiline values or ignored. + +        Configuration files may include comments, prefixed by specific +        characters (`#' and `;' by default). Comments may appear on their own +        in an otherwise empty line or may be entered in lines holding values or +        section names.          """ -        cursect = None                            # None, or a dictionary +        elements_added = set() +        cursect = None                        # None, or a dictionary +        sectname = None          optname = None          lineno = 0 -        e = None                                  # None, or an exception -        while True: -            line = fp.readline() -            if not line: -                break -            lineno = lineno + 1 -            # comment or blank line? -            if line.strip() == '' or line[0] in '#;': -                continue -            if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR": -                # no leading whitespace +        indent_level = 0 +        e = None                              # None, or an exception +        for lineno, line in enumerate(fp, start=1): +            comment_start = None +            # strip inline comments +            for prefix in self._inline_comment_prefixes: +                index = line.find(prefix) +                if index == 0 or (index > 0 and line[index-1].isspace()): +                    comment_start = index +                    break +            # strip full line comments +            for prefix in self._comment_prefixes: +                if line.strip().startswith(prefix): +                    comment_start = 0 +                    break +            value = line[:comment_start].strip() +            if not value: +                if self._empty_lines_in_values: +                    # add empty line to the value, but only if there was no +                    # comment on the line +                    if (comment_start is None and +                        cursect is not None and +                        optname and +                        cursect[optname] is not None): +                        cursect[optname].append('') # newlines added at join +                else: +                    # empty line marks end of value +                    indent_level = sys.maxsize                  continue              # continuation line? -            if line[0].isspace() and cursect is not None and optname: -                value = line.strip() -                if value: -                    cursect[optname] = "%s\n%s" % (cursect[optname], value) +            first_nonspace = self.NONSPACECRE.search(line) +            cur_indent_level = first_nonspace.start() if first_nonspace else 0 +            if (cursect is not None and optname and +                cur_indent_level > indent_level): +                cursect[optname].append(value)              # a section header or option header?              else: +                indent_level = cur_indent_level                  # is it a section header? -                mo = self.SECTCRE.match(line) +                mo = self.SECTCRE.match(value)                  if mo:                      sectname = mo.group('header')                      if sectname in self._sections: +                        if self._strict and sectname in elements_added: +                            raise DuplicateSectionError(sectname, fpname, +                                                        lineno)                          cursect = self._sections[sectname] -                    elif sectname == DEFAULTSECT: +                        elements_added.add(sectname) +                    elif sectname == self.default_section:                          cursect = self._defaults                      else:                          cursect = self._dict() -                        cursect['__name__'] = sectname                          self._sections[sectname] = cursect +                        self._proxies[sectname] = SectionProxy(self, sectname) +                        elements_added.add(sectname)                      # So sections can't start with a continuation line                      optname = None                  # no section header in the file? @@ -488,253 +1051,197 @@ class RawConfigParser:                      raise MissingSectionHeaderError(fpname, lineno, line)                  # an option line?                  else: -                    mo = self.OPTCRE.match(line) +                    mo = self._optcre.match(value)                      if mo:                          optname, vi, optval = mo.group('option', 'vi', 'value') -                        if vi in ('=', ':') and ';' in optval: -                            # ';' is a comment delimiter only if it follows -                            # a spacing character -                            pos = optval.find(';') -                            if pos != -1 and optval[pos-1].isspace(): -                                optval = optval[:pos] -                        optval = optval.strip() -                        # allow empty values -                        if optval == '""': -                            optval = '' +                        if not optname: +                            e = self._handle_error(e, fpname, lineno, line)                          optname = self.optionxform(optname.rstrip()) -                        cursect[optname] = optval +                        if (self._strict and +                            (sectname, optname) in elements_added): +                            raise DuplicateOptionError(sectname, optname, +                                                       fpname, lineno) +                        elements_added.add((sectname, optname)) +                        # This check is fine because the OPTCRE cannot +                        # match if it would set optval to None +                        if optval is not None: +                            optval = optval.strip() +                            cursect[optname] = [optval] +                        else: +                            # valueless option handling +                            cursect[optname] = None                      else: -                        # a non-fatal parsing error occurred.  set up the +                        # a non-fatal parsing error occurred. set up the                          # exception but keep going. the exception will be                          # raised at the end of the file and will contain a                          # list of all bogus lines -                        if not e: -                            e = ParsingError(fpname) -                        e.append(lineno, repr(line)) +                        e = self._handle_error(e, fpname, lineno, line)          # if any parsing errors occurred, raise an exception          if e:              raise e +        self._join_multiline_values() + +    def _join_multiline_values(self): +        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): +                    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: +            exc = ParsingError(fpname) +        exc.append(lineno, repr(line)) +        return exc + +    def _unify_values(self, section, vars): +        """Create a sequence of lookups with 'vars' taking priority over +        the 'section' which takes priority over the DEFAULTSECT. -class _Chainmap: -    """Combine multiple mappings for successive lookups. - -    For example, to emulate Python's normal lookup sequence: - -        import __builtin__ -        pylookup = _Chainmap(locals(), globals(), vars(__builtin__)) -    """ - -    def __init__(self, *maps): -        self.maps = maps - -    def __getitem__(self, key): -        for mapping in self.maps: -            try: -                return mapping[key] -            except KeyError: -                pass -        raise KeyError(key) - -    def __iter__(self): -        seen = set() -        for mapping in self.maps: -            s = set(mapping) - seen -            for elem in s: -                yield elem -            seen.update(s) - -    def __len__(self): -        s = set() -        s.update(*self.maps) -        return len(s) - -    def get(self, key, default=None): +        """ +        sectiondict = {}          try: -            return self[key] +            sectiondict = self._sections[section]          except KeyError: -            return default +            if section != self.default_section: +                raise NoSectionError(section) +        # Update with the entry specific variables +        vardict = {} +        if vars: +            for key, value in vars.items(): +                if value is not None: +                    value = str(value) +                vardict[self.optionxform(key)] = value +        return _ChainMap(vardict, sectiondict, self._defaults) -    def __contains__(self, key): -        try: -            self[key] -        except KeyError: -            return False -        else: -            return True +    def _convert_to_boolean(self, value): +        """Return a boolean value translating from other types if necessary. +        """ +        if value.lower() not in self.BOOLEAN_STATES: +            raise ValueError('Not a boolean: %s' % value) +        return self.BOOLEAN_STATES[value.lower()] + +    def _validate_value_types(self, *, section="", option="", value=""): +        """Raises a TypeError for non-string values. + +        The only legal non-string value if we allow valueless +        options is None, so we need to check if the value is a +        string if: +        - we do not allow valueless options, or +        - we allow valueless options but the value is not None + +        For compatibility reasons this method is not used in classic set() +        for RawConfigParsers. It is invoked in every case for mapping protocol +        access and in ConfigParser.set(). +        """ +        if not isinstance(section, str): +            raise TypeError("section names must be strings") +        if not isinstance(option, str): +            raise TypeError("option keys must be strings") +        if not self._allow_no_value or value: +            if not isinstance(value, str): +                raise TypeError("option values must be strings") -    def keys(self): -        return list(self) -    def items(self): -        return [(k, self[k]) for k in self] +class ConfigParser(RawConfigParser): +    """ConfigParser implementing interpolation.""" -    def values(self): -        return [self[k] for k in self] +    _DEFAULT_INTERPOLATION = BasicInterpolation() -    def __eq__(self, other): -        return dict(self.items()) == dict(other.items()) +    def set(self, section, option, value=None): +        """Set an option.  Extends RawConfigParser.set by validating type and +        interpolation syntax on the value.""" +        self._validate_value_types(option=option, value=value) +        super().set(section, option, value) -    def __ne__(self, other): -        return not (self == other) +    def add_section(self, section): +        """Create a new section in the configuration.  Extends +        RawConfigParser.add_section by validating if the section name is +        a string.""" +        self._validate_value_types(section=section) +        super().add_section(section) -class ConfigParser(RawConfigParser): +class SafeConfigParser(ConfigParser): +    """ConfigParser alias for backwards compatibility purposes.""" + +    def __init__(self, *args, **kwargs): +        super().__init__(*args, **kwargs) +        warnings.warn( +            "The SafeConfigParser class has been renamed to ConfigParser " +            "in Python 3.2. This alias will be removed in future versions." +            " Use ConfigParser directly instead.", +            DeprecationWarning, stacklevel=2 +        ) -    def get(self, section, option, raw=False, vars=None): -        """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 `defaults' in that order. +class SectionProxy(MutableMapping): +    """A proxy for a single section from a parser.""" -        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. +    def __init__(self, parser, name): +        """Creates a view on a section of the specified `name` in `parser`.""" +        self._parser = parser +        self._name = name -        The section DEFAULT is special. -        """ -        sectiondict = {} -        try: -            sectiondict = self._sections[section] -        except KeyError: -            if section != DEFAULTSECT: -                raise NoSectionError(section) -        # Update with the entry specific variables -        vardict = {} -        if vars: -            for key, value in vars.items(): -                vardict[self.optionxform(key)] = value -        d = _Chainmap(vardict, sectiondict, self._defaults) -        option = self.optionxform(option) -        try: -            value = d[option] -        except KeyError: -            raise NoOptionError(option, section) +    def __repr__(self): +        return '<Section: {}>'.format(self._name) -        if raw: -            return value -        else: -            return self._interpolate(section, option, value, d) +    def __getitem__(self, key): +        if not self._parser.has_option(self._name, key): +            raise KeyError(key) +        return self._parser.get(self._name, key) -    def items(self, section, raw=False, vars=None): -        """Return a list of tuples with (name, value) for each option -        in the section. +    def __setitem__(self, key, value): +        self._parser._validate_value_types(option=key, value=value) +        return self._parser.set(self._name, key, value) -        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. +    def __delitem__(self, key): +        if not (self._parser.has_option(self._name, key) and +                self._parser.remove_option(self._name, key)): +            raise KeyError(key) -        The section DEFAULT is special. -        """ -        d = self._defaults.copy() -        try: -            d.update(self._sections[section]) -        except KeyError: -            if section != DEFAULTSECT: -                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 "__name__" in options: -            options.remove("__name__") -        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 __contains__(self, key): +        return self._parser.has_option(self._name, key) -    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 "%(" 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 "%(" in value: -            raise InterpolationDepthError(option, section, rawval) -        return value +    def __len__(self): +        return len(self._options()) -    _KEYCRE = re.compile(r"%\(([^)]*)\)s|.") +    def __iter__(self): +        return self._options().__iter__() -    def _interpolation_replace(self, match): -        s = match.group(1) -        if s is None: -            return match.group() +    def _options(self): +        if self._name != self._parser.default_section: +            return self._parser.options(self._name)          else: -            return "%%(%s)s" % self.optionxform(s) +            return self._parser.defaults() +    def get(self, option, fallback=None, *, raw=False, vars=None): +        return self._parser.get(self._name, option, raw=raw, vars=vars, +                                fallback=fallback) -class SafeConfigParser(ConfigParser): +    def getint(self, option, fallback=None, *, raw=False, vars=None): +        return self._parser.getint(self._name, option, raw=raw, vars=vars, +                                   fallback=fallback) -    def _interpolate(self, section, option, rawval, vars): -        # do the string interpolation -        L = [] -        self._interpolate_some(option, L, rawval, section, vars, 1) -        return ''.join(L) +    def getfloat(self, option, fallback=None, *, raw=False, vars=None): +        return self._parser.getfloat(self._name, option, raw=raw, vars=vars, +                                     fallback=fallback) -    _interpvar_re = re.compile(r"%\(([^)]+)\)s") +    def getboolean(self, option, fallback=None, *, raw=False, vars=None): +        return self._parser.getboolean(self._name, option, raw=raw, vars=vars, +                                       fallback=fallback) -    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): -        """Set an option.  Extend ConfigParser.set: check for string values.""" -        if not isinstance(value, str): -            raise TypeError("option values must be strings") -        # check for bad percent signs: -        # first, replace all "good" interpolations -        tmp_value = value.replace('%%', '') -        tmp_value = self._interpvar_re.sub('', tmp_value) -        # then, check if there's a lone percent sign left -        percent_index = tmp_value.find('%') -        if percent_index != -1: -            raise ValueError("invalid interpolation syntax in %r at " -                             "position %d" % (value, percent_index)) -        ConfigParser.set(self, section, option, value) +    @property +    def parser(self): +        # The parser object of the proxy is read-only. +        return self._parser + +    @property +    def name(self): +        # The name of the section on a proxy is read-only. +        return self._name | 
