diff options
Diffstat (limited to 'Lib/configparser.py')
-rw-r--r-- | Lib/configparser.py | 318 |
1 files changed, 211 insertions, 107 deletions
diff --git a/Lib/configparser.py b/Lib/configparser.py index 2fbedf82f5..d979e6c086 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -1,6 +1,6 @@ """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. @@ -24,67 +24,88 @@ ConfigParser -- responsible for parsing a list of 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, + delimiters=('=', ':'), comment_prefixes=('#', ';'), + empty_lines_in_values=True, allow_no_value=False): + 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. Note that `__name__' + is always an intrinsic default; its value is the section's name. + + 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 a line. + + 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 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. + 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). get(section, option, raw=False, vars=None) - return a string value for the named option. All % interpolations are + 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. getint(section, options) - like get(), but convert value to an integer + Like get(), but convert value to an integer. getfloat(section, options) - like get(), but convert value to a float + Like get(), but convert value to a float. getboolean(section, options) - like get(), but convert value to a boolean (currently case + 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) - return a list of tuples with (name, value) for each option + 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: @@ -94,6 +115,7 @@ except ImportError: _default_dict = dict import re +import sys __all__ = ["NoSectionError", "DuplicateSectionError", "NoOptionError", "InterpolationError", "InterpolationDepthError", @@ -114,17 +136,19 @@ 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 - # DeprecationWarning from popping up over this pre-existing attribute, use - # a new property that takes lookup precedence. + # DeprecationWarning from popping up over this pre-existing attribute, use a + # new property that takes lookup precedence. message = property(_get_message, _set_message) def __init__(self, msg=''): @@ -136,6 +160,7 @@ class Error(Exception): __str__ = __repr__ + class NoSectionError(Error): """Raised when no section matches a requested option.""" @@ -144,6 +169,7 @@ class NoSectionError(Error): self.section = section self.args = (section, ) + class DuplicateSectionError(Error): """Raised when a section is multiply-created.""" @@ -152,6 +178,7 @@ class DuplicateSectionError(Error): self.section = section self.args = (section, ) + class NoOptionError(Error): """A requested option was not found.""" @@ -162,6 +189,7 @@ class NoOptionError(Error): self.section = section self.args = (option, section) + class InterpolationError(Error): """Base class for interpolation-related exceptions.""" @@ -171,6 +199,7 @@ class InterpolationError(Error): self.section = section self.args = (option, section, msg) + class InterpolationMissingOptionError(InterpolationError): """A string substitution required a setting which was not available.""" @@ -185,10 +214,12 @@ class InterpolationMissingOptionError(InterpolationError): 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.""" + class InterpolationDepthError(InterpolationError): """Raised when substitutions are nested too deeply.""" @@ -201,6 +232,7 @@ class InterpolationDepthError(InterpolationError): InterpolationError.__init__(self, option, section, msg) self.args = (option, section, rawval) + class ParsingError(Error): """Raised when a configuration file does not follow legal syntax.""" @@ -214,6 +246,7 @@ class ParsingError(Error): 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.""" @@ -227,19 +260,74 @@ class MissingSectionHeaderError(ParsingError): self.line = line self.args = (filename, lineno, line) + class RawConfigParser: + """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 + """ + + # 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") + # Select backwards-compatible inline comment character behavior + # (; and # are comments at the start of a line, but ; only inline) + _COMPATIBLE = object() + def __init__(self, defaults=None, dict_type=_default_dict, - allow_no_value=False): + delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, + empty_lines_in_values=True, allow_no_value=False): self._dict = dict_type self._sections = self._dict() self._defaults = self._dict() - if allow_no_value: - self._optcre = self.OPTCRE_NV - else: - self._optcre = self.OPTCRE 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: + delim = "|".join(re.escape(d) for d in delimiters) + if allow_no_value: + self._optcre = re.compile(self._OPT_NV_TMPL.format(delim=delim), + re.VERBOSE) + else: + self._optcre = re.compile(self._OPT_TMPL.format(delim=delim), + re.VERBOSE) + if comment_prefixes is self._COMPATIBLE: + self._startonly_comment_prefixes = ('#',) + self._comment_prefixes = (';',) + else: + self._startonly_comment_prefixes = () + self._comment_prefixes = tuple(comment_prefixes or ()) + self._empty_lines_in_values = empty_lines_in_values def defaults(self): return self._defaults @@ -313,7 +401,6 @@ class RawConfigParser: second argument is the `filename', which if not given, is taken from fp.name. If fp has no `name' attribute, `<???>' is used. - """ if filename is None: try: @@ -374,6 +461,7 @@ class RawConfigParser: def has_option(self, section, option): """Check for the existence of a given option in a given section.""" + if not section or section == DEFAULTSECT: option = self.optionxform(option) return option in self._defaults @@ -386,6 +474,7 @@ class RawConfigParser: def set(self, section, option, value=None): """Set an option.""" + if not section or section == DEFAULTSECT: sectdict = self._defaults else: @@ -395,22 +484,34 @@ 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, DEFAULTSECT, 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__": - continue - if value is not None: - key = " = ".join((key, str(value).replace('\n', '\n\t'))) - fp.write("%s\n" % (key)) - 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: + if key == "__name__": + continue + if value is not None: + 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.""" @@ -434,66 +535,63 @@ class RawConfigParser: del self._sections[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 - ) - OPTCRE_NV = re.compile( - r'(?P<option>[^:=\s][^:=]*)' # very permissive! - r'\s*(?:' # any number of space/tab, - r'(?P<vi>[:=])\s*' # optionally followed by - # separator (either : or - # =), followed by any # - # space/tab - r'(?P<value>.*))?$' # everything up to eol - ) - 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 optname = None lineno = 0 + indent_level = 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 + for lineno, line in enumerate(fp, start=1): + # strip prefix-only comments + comment_start = None + for prefix in self._startonly_comment_prefixes: + if line.strip().startswith(prefix): + comment_start = 0 + break + # strip inline comments + for prefix in self._comment_prefixes: + index = line.find(prefix) + if index == 0 or (index > 0 and line[index-1].isspace()): + comment_start = index + break + value = line[:comment_start].strip() + if not value: + if self._empty_lines_in_values and comment_start is None: + # add empty line to the value, but only if there was no + # comment on the line + if cursect is not None and optname: + cursect[optname].append('\n') + 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].append(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: @@ -511,19 +609,15 @@ 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 not optname: + e = self._handle_error(e, fpname, lineno, line) optname = self.optionxform(optname.rstrip()) # This check is fine because the OPTCRE cannot # match if it would set optval to None if optval is not None: - 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 == '""': @@ -533,26 +627,35 @@ class RawConfigParser: # valueless option handling cursect[optname] = optval 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() - # join the multi-line values collected while reading + def _join_multiline_values(self): all_sections = [self._defaults] all_sections.extend(self._sections.values()) for options in all_sections: for name, val in options.items(): if isinstance(val, list): + if val[-1] == '\n': + val = val[:-1] options[name] = '\n'.join(val) + def _handle_error(self, exc, fpname, lineno, line): + if not exc: + exc = ParsingError(fpname) + exc.append(lineno, repr(line)) + return exc + + class ConfigParser(RawConfigParser): + """ConfigParser implementing interpolation.""" def get(self, section, option, raw=False, vars=None): """Get an option value for a given section. @@ -648,6 +751,7 @@ class ConfigParser(RawConfigParser): class SafeConfigParser(ConfigParser): + """ConfigParser implementing sane interpolation.""" def _interpolate(self, section, option, rawval, vars): # do the string interpolation |