Add read_file, read_string, and read_dict to the configparser API;
new source attribute to exceptions.
This commit is contained in:
Fred Drake 2010-08-09 12:52:45 +00:00
parent f14c263280
commit a492362f9a
4 changed files with 451 additions and 141 deletions

View File

@ -27,8 +27,9 @@ customized by end users easily.
the Windows Registry extended version of INI syntax. the Windows Registry extended version of INI syntax.
A configuration file consists of sections, each led by a ``[section]`` header, A configuration file consists of sections, each led by a ``[section]`` header,
followed by name/value entries separated by a specific string (``=`` or ``:`` by followed by key/value entries separated by a specific string (``=`` or ``:`` by
default). Note that leading whitespace is removed from values. Values can be default). By default, section names are case sensitive but keys are not. Leading
und trailing whitespace is removed from keys and from values. Values can be
ommitted, in which case the key/value delimiter may also be left out. Values ommitted, in which case the key/value delimiter may also be left out. Values
can also span multiple lines, as long as they are indented deeper than the first can also 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 line of the value. Depending on the parser's mode, blank lines may be treated
@ -101,7 +102,7 @@ that sorts its keys, the sections will be sorted on write-back, as will be the
keys within each section. keys within each section.
.. class:: RawConfigParser(defaults=None, dict_type=collections.OrderedDict, delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, empty_lines_in_values=True, allow_no_value=False) .. class:: RawConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, strict=False, empty_lines_in_values=True)
The basic configuration object. When *defaults* is given, it is initialized The basic configuration object. When *defaults* is given, it is initialized
into the dictionary of intrinsic defaults. When *dict_type* is given, it into the dictionary of intrinsic defaults. When *dict_type* is given, it
@ -115,11 +116,14 @@ keys within each section.
*comment_prefixes* is a special value that indicates that ``;`` and ``#`` can *comment_prefixes* is a special value that indicates that ``;`` and ``#`` can
start whole line comments while only ``;`` can start inline comments. start whole line comments while only ``;`` can start inline comments.
When *empty_lines_in_values* is ``False`` (default: ``True``), each empty When *strict* is ``True`` (default: ``False``), the parser won't allow for
line marks the end of an option. Otherwise, internal empty lines of a any section or option duplicates while reading from a single source (file,
multiline option are kept as part of the value. When *allow_no_value* is string or dictionary), raising :exc:`DuplicateSectionError` or
true (default: ``False``), options without values are accepted; the value :exc:`DuplicateOptionError`. When *empty_lines_in_values* is ``False``
presented for these is ``None``. (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``.
This class does not support the magical interpolation behavior. This class does not support the magical interpolation behavior.
@ -127,11 +131,11 @@ keys within each section.
The default *dict_type* is :class:`collections.OrderedDict`. The default *dict_type* is :class:`collections.OrderedDict`.
.. versionchanged:: 3.2 .. versionchanged:: 3.2
*delimiters*, *comment_prefixes*, *empty_lines_in_values* and *allow_no_value*, *delimiters*, *comment_prefixes*, *strict* and
*allow_no_value* were added. *empty_lines_in_values* were added.
.. class:: SafeConfigParser(defaults=None, dict_type=collections.OrderedDict, delimiters=('=', ':'), comment_prefixes=('#', ';'), empty_lines_in_values=True, allow_no_value=False) .. class:: SafeConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=('#', ';'), strict=False, empty_lines_in_values=True)
Derived class of :class:`ConfigParser` that implements a sane variant of the Derived class of :class:`ConfigParser` that implements a sane variant of the
magical interpolation feature. This implementation is more predictable as it magical interpolation feature. This implementation is more predictable as it
@ -147,11 +151,11 @@ keys within each section.
The default *dict_type* is :class:`collections.OrderedDict`. The default *dict_type* is :class:`collections.OrderedDict`.
.. versionchanged:: 3.2 .. versionchanged:: 3.2
*delimiters*, *comment_prefixes*, *empty_lines_in_values* and *allow_no_value*, *delimiters*, *comment_prefixes*, *strict* and
*allow_no_value* were added. *empty_lines_in_values* were added.
.. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict, delimiters=('=', ':'), comment_prefixes=('#', ';'), empty_lines_in_values=True, allow_no_value=False) .. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=('#', ';'), strict=False, empty_lines_in_values=True)
Derived class of :class:`RawConfigParser` that implements the magical Derived class of :class:`RawConfigParser` that implements the magical
interpolation feature and adds optional arguments to the :meth:`get` and interpolation feature and adds optional arguments to the :meth:`get` and
@ -174,8 +178,8 @@ keys within each section.
The default *dict_type* is :class:`collections.OrderedDict`. The default *dict_type* is :class:`collections.OrderedDict`.
.. versionchanged:: 3.2 .. versionchanged:: 3.2
*delimiters*, *comment_prefixes*, *empty_lines_in_values* and *allow_no_value*, *delimiters*, *comment_prefixes*,
*allow_no_value* were added. *strict* and *empty_lines_in_values* were added.
.. exception:: Error .. exception:: Error
@ -191,12 +195,26 @@ keys within each section.
.. exception:: DuplicateSectionError .. exception:: DuplicateSectionError
Exception raised if :meth:`add_section` is called with the name of a section Exception raised if :meth:`add_section` is called with the name of a section
that is already present. that is already present or in strict parsers when a section if found more
than once in a single input file, string or dictionary.
.. versionadded:: 3.2
Optional ``source`` and ``lineno`` attributes and arguments to
:meth:`__init__` were added.
.. exception:: DuplicateOptionError
Exception raised by strict parsers if a single option appears twice during
reading from a single file, string or dictionary. This catches misspellings
and case sensitivity-related errors, e.g. a dictionary may have two keys
representing the same case-insensitive configuration key.
.. exception:: NoOptionError .. exception:: NoOptionError
Exception raised when a specified option is not found in the specified section. Exception raised when a specified option is not found in the specified
section.
.. exception:: InterpolationError .. exception:: InterpolationError
@ -233,6 +251,9 @@ keys within each section.
Exception raised when errors occur attempting to parse a file. Exception raised when errors occur attempting to parse a file.
.. versionchanged:: 3.2
The ``filename`` attribute and :meth:`__init__` argument were renamed to
``source`` for consistency.
.. data:: MAX_INTERPOLATION_DEPTH .. data:: MAX_INTERPOLATION_DEPTH
@ -315,15 +336,41 @@ RawConfigParser Objects
default encoding for :func:`open`. default encoding for :func:`open`.
.. method:: RawConfigParser.readfp(fp, filename=None) .. method:: RawConfigParser.read_file(f, source=None)
Read and parse configuration data from the file or file-like object in *fp* Read and parse configuration data from the file or file-like object in *f*
(only the :meth:`readline` method is used). The file-like object must (only the :meth:`readline` method is used). The file-like object must
operate in text mode, i.e. return strings from :meth:`readline`. operate in text mode, i.e. return strings from :meth:`readline`.
If *filename* is omitted and *fp* has a :attr:`name` attribute, that is used Optional argument *source* specifies the name of the file being read. It not
for *filename*; the default is ``<???>``. given and *f* has a :attr:`name` attribute, that is used for *source*; the
default is ``<???>``.
.. versionadded:: 3.2
Renamed from :meth:`readfp` (with the ``filename`` attribute renamed to
``source`` for consistency with other ``read_*`` methods).
.. method:: RawConfigParser.read_string(string, source='<string>')
Parse configuration data from a given string.
Optional argument *source* specifies a context-specific name of the string
passed. If not given, ``<string>`` is used.
.. versionadded:: 3.2
.. method:: RawConfigParser.read_dict(dictionary, source='<dict>')
Load 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.
Optional argument *source* specifies a context-specific name of the
dictionary passed. If not given, ``<dict>`` is used.
.. versionadded:: 3.2
.. method:: RawConfigParser.get(section, option) .. method:: RawConfigParser.get(section, option)
@ -408,6 +455,10 @@ RawConfigParser Objects
Note that when reading configuration files, whitespace around the Note that when reading configuration files, whitespace around the
option names are stripped before :meth:`optionxform` is called. option names are stripped before :meth:`optionxform` is called.
.. method:: RawConfigParser.readfp(fp, filename=None)
.. deprecated:: 3.2
Please use :meth:`read_file` instead.
.. _configparser-objects: .. _configparser-objects:

View File

@ -26,10 +26,10 @@ ConfigParser -- responsible for parsing a list of
__init__(defaults=None, dict_type=_default_dict, __init__(defaults=None, dict_type=_default_dict,
delimiters=('=', ':'), comment_prefixes=('#', ';'), delimiters=('=', ':'), comment_prefixes=('#', ';'),
empty_lines_in_values=True, allow_no_value=False): strict=False, empty_lines_in_values=True, allow_no_value=False):
Create the parser. When `defaults' is given, it is initialized into the Create the parser. When `defaults' is given, it is initialized into the
dictionary or intrinsic defaults. The keys must be strings, the values dictionary or intrinsic defaults. The keys must be strings, the values
must be appropriate for %()s string interpolation. Note that `__name__' must be appropriate for %()s string interpolation. Note that `__name__'
is always an intrinsic default; its value is the section's 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 When `dict_type' is given, it will be used to create the dictionary
@ -42,6 +42,10 @@ ConfigParser -- responsible for parsing a list of
When `comment_prefixes' is given, it will be used as the set of When `comment_prefixes' is given, it will be used as the set of
substrings that prefix comments in a line. substrings that prefix comments in a line.
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 False.
When `empty_lines_in_values' is False (default: True), each empty line When `empty_lines_in_values' is False (default: True), each empty line
marks the end of an option. Otherwise, internal empty lines of marks the end of an option. Otherwise, internal empty lines of
a multiline option are kept as part of the value. a multiline option are kept as part of the value.
@ -66,10 +70,19 @@ ConfigParser -- responsible for parsing a list of
name. A single filename is also allowed. Non-existing files name. A single filename is also allowed. Non-existing files
are ignored. Return list of successfully read files. are ignored. Return list of successfully read files.
readfp(fp, filename=None) read_file(f, 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 The filename defaults to f.name; it is only used in error
messages (if fp has no `name' attribute, the string `<???>' is used). messages (if f has no `name' attribute, the string `<???>' is used).
read_string(string)
Read configuration from a given string.
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.
get(section, option, raw=False, vars=None) 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
@ -114,11 +127,13 @@ except ImportError:
# fallback for setup.py which hasn't yet built _collections # fallback for setup.py which hasn't yet built _collections
_default_dict = dict _default_dict = dict
import io
import re import re
import sys import sys
import warnings
__all__ = ["NoSectionError", "DuplicateSectionError", "NoOptionError", __all__ = ["NoSectionError", "DuplicateOptionError", "DuplicateSectionError",
"InterpolationError", "InterpolationDepthError", "NoOptionError", "InterpolationError", "InterpolationDepthError",
"InterpolationSyntaxError", "ParsingError", "InterpolationSyntaxError", "ParsingError",
"MissingSectionHeaderError", "MissingSectionHeaderError",
"ConfigParser", "SafeConfigParser", "RawConfigParser", "ConfigParser", "SafeConfigParser", "RawConfigParser",
@ -147,8 +162,8 @@ class Error(Exception):
self.__message = value self.__message = value
# BaseException.message has been deprecated since Python 2.6. To prevent # BaseException.message has been deprecated since Python 2.6. To prevent
# DeprecationWarning from popping up over this pre-existing attribute, use a # DeprecationWarning from popping up over this pre-existing attribute, use
# new property that takes lookup precedence. # a new property that takes lookup precedence.
message = property(_get_message, _set_message) message = property(_get_message, _set_message)
def __init__(self, msg=''): def __init__(self, msg=''):
@ -171,12 +186,56 @@ class NoSectionError(Error):
class DuplicateSectionError(Error): class DuplicateSectionError(Error):
"""Raised when a section is multiply-created.""" """Raised when a section is repeated in an input source.
def __init__(self, section): Possible repetitions that raise this exception are: multiple creation
Error.__init__(self, "Section %r already exists" % section) 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.section = section
self.args = (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): class NoOptionError(Error):
@ -216,8 +275,12 @@ class InterpolationMissingOptionError(InterpolationError):
class InterpolationSyntaxError(InterpolationError): class InterpolationSyntaxError(InterpolationError):
"""Raised when the source text into which substitutions are made """Raised when the source text contains invalid syntax.
does not conform to the required syntax."""
Current implementation raises this exception only for SafeConfigParser
instances when the source text into which substitutions are made
does not conform to the required syntax.
"""
class InterpolationDepthError(InterpolationError): class InterpolationDepthError(InterpolationError):
@ -236,11 +299,40 @@ class InterpolationDepthError(InterpolationError):
class ParsingError(Error): class ParsingError(Error):
"""Raised when a configuration file does not follow legal syntax.""" """Raised when a configuration file does not follow legal syntax."""
def __init__(self, filename): def __init__(self, source=None, filename=None):
Error.__init__(self, 'File contains parsing errors: %s' % filename) # Exactly one of `source'/`filename' arguments has to be given.
self.filename = filename # `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.errors = []
self.args = (filename, ) self.args = (source, )
@property
def filename(self):
"""Deprecated, use `source'."""
warnings.warn(
"This 'filename' attribute will be removed in future versions. "
"Use 'source' instead.",
PendingDeprecationWarning, 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.",
PendingDeprecationWarning, stacklevel=2
)
self.source = value
def append(self, lineno, line): def append(self, lineno, line):
self.errors.append((lineno, line)) self.errors.append((lineno, line))
@ -255,7 +347,7 @@ class MissingSectionHeaderError(ParsingError):
self, self,
'File contains no section headers.\nfile: %s, line: %d\n%r' % 'File contains no section headers.\nfile: %s, line: %d\n%r' %
(filename, lineno, line)) (filename, lineno, line))
self.filename = filename self.source = filename
self.lineno = lineno self.lineno = lineno
self.line = line self.line = line
self.args = (filename, lineno, line) self.args = (filename, lineno, line)
@ -302,8 +394,9 @@ class RawConfigParser:
_COMPATIBLE = object() _COMPATIBLE = object()
def __init__(self, defaults=None, dict_type=_default_dict, def __init__(self, defaults=None, dict_type=_default_dict,
delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, allow_no_value=False, *, delimiters=('=', ':'),
empty_lines_in_values=True, allow_no_value=False): comment_prefixes=_COMPATIBLE, strict=False,
empty_lines_in_values=True):
self._dict = dict_type self._dict = dict_type
self._sections = self._dict() self._sections = self._dict()
self._defaults = self._dict() self._defaults = self._dict()
@ -314,12 +407,12 @@ class RawConfigParser:
if delimiters == ('=', ':'): if delimiters == ('=', ':'):
self._optcre = self.OPTCRE_NV if allow_no_value else self.OPTCRE self._optcre = self.OPTCRE_NV if allow_no_value else self.OPTCRE
else: else:
delim = "|".join(re.escape(d) for d in delimiters) d = "|".join(re.escape(d) for d in delimiters)
if allow_no_value: if allow_no_value:
self._optcre = re.compile(self._OPT_NV_TMPL.format(delim=delim), self._optcre = re.compile(self._OPT_NV_TMPL.format(delim=d),
re.VERBOSE) re.VERBOSE)
else: else:
self._optcre = re.compile(self._OPT_TMPL.format(delim=delim), self._optcre = re.compile(self._OPT_TMPL.format(delim=d),
re.VERBOSE) re.VERBOSE)
if comment_prefixes is self._COMPATIBLE: if comment_prefixes is self._COMPATIBLE:
self._startonly_comment_prefixes = ('#',) self._startonly_comment_prefixes = ('#',)
@ -327,6 +420,7 @@ class RawConfigParser:
else: else:
self._startonly_comment_prefixes = () self._startonly_comment_prefixes = ()
self._comment_prefixes = tuple(comment_prefixes or ()) self._comment_prefixes = tuple(comment_prefixes or ())
self._strict = strict
self._empty_lines_in_values = empty_lines_in_values self._empty_lines_in_values = empty_lines_in_values
def defaults(self): def defaults(self):
@ -394,20 +488,59 @@ class RawConfigParser:
read_ok.append(filename) read_ok.append(filename)
return read_ok 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. """Like read() but the argument must be a file-like object.
The `fp' argument must have a `readline' method. Optional The `f' argument must have a `readline' method. Optional second
second argument is the `filename', which if not given, is argument is the `source' specifying the name of the file being read. If
taken from fp.name. If fp has no `name' attribute, `<???>' is not given, it is taken from f.name. If `f' has no `name' attribute,
used. `<???>' is used.
""" """
if filename is None: if source is None:
try: try:
filename = fp.name srouce = f.name
except AttributeError: except AttributeError:
filename = '<???>' source = '<???>'
self._read(fp, filename) self._read(f, source)
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.
Optional second argument is the `source' specifying the name of the
dictionary being read.
"""
elements_added = set()
for section, keys in dictionary.items():
try:
self.add_section(section)
except DuplicateSectionError:
if self._strict and section in elements_added:
raise
elements_added.add(section)
for key, value in keys.items():
key = self.optionxform(key)
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.",
PendingDeprecationWarning, stacklevel=2
)
self.read_file(fp, source=filename)
def get(self, section, option): def get(self, section, option):
opt = self.optionxform(option) opt = self.optionxform(option)
@ -461,7 +594,6 @@ class RawConfigParser:
def has_option(self, section, option): def has_option(self, section, option):
"""Check for the existence of a given option in a given section.""" """Check for the existence of a given option in a given section."""
if not section or section == DEFAULTSECT: if not section or section == DEFAULTSECT:
option = self.optionxform(option) option = self.optionxform(option)
return option in self._defaults return option in self._defaults
@ -474,7 +606,6 @@ class RawConfigParser:
def set(self, section, option, value=None): def set(self, section, option, value=None):
"""Set an option.""" """Set an option."""
if not section or section == DEFAULTSECT: if not section or section == DEFAULTSECT:
sectdict = self._defaults sectdict = self._defaults
else: else:
@ -538,21 +669,23 @@ class RawConfigParser:
def _read(self, fp, fpname): def _read(self, fp, fpname):
"""Parse a sectioned configuration file. """Parse a sectioned configuration file.
Each section in a configuration file contains a header, indicated by a Each section in a configuration file contains a header, indicated by
name in square brackets (`[]'), plus key/value options, indicated by a name in square brackets (`[]'), plus key/value options, indicated by
`name' and `value' delimited with a specific substring (`=' or `:' by `name' and `value' delimited with a specific substring (`=' or `:' by
default). default).
Values can span multiple lines, as long as they are indented deeper than Values can span multiple lines, as long as they are indented deeper
the first line of the value. Depending on the parser's mode, blank lines than the first line of the value. Depending on the parser's mode, blank
may be treated as parts of multiline values or ignored. lines may be treated as parts of multiline values or ignored.
Configuration files may include comments, prefixed by specific Configuration files may include comments, prefixed by specific
characters (`#' and `;' by default). Comments may appear on their own in characters (`#' and `;' by default). Comments may appear on their own
an otherwise empty line or may be entered in lines holding values or in an otherwise empty line or may be entered in lines holding values or
section names. section names.
""" """
elements_added = set()
cursect = None # None, or a dictionary cursect = None # None, or a dictionary
sectname = None
optname = None optname = None
lineno = 0 lineno = 0
indent_level = 0 indent_level = 0
@ -598,13 +731,18 @@ class RawConfigParser:
if mo: if mo:
sectname = mo.group('header') sectname = mo.group('header')
if sectname in self._sections: if sectname in self._sections:
if self._strict and sectname in elements_added:
raise DuplicateSectionError(sectname, fpname,
lineno)
cursect = self._sections[sectname] cursect = self._sections[sectname]
elements_added.add(sectname)
elif sectname == DEFAULTSECT: elif sectname == DEFAULTSECT:
cursect = self._defaults cursect = self._defaults
else: else:
cursect = self._dict() cursect = self._dict()
cursect['__name__'] = sectname cursect['__name__'] = sectname
self._sections[sectname] = cursect self._sections[sectname] = cursect
elements_added.add(sectname)
# So sections can't start with a continuation line # So sections can't start with a continuation line
optname = None optname = None
# no section header in the file? # no section header in the file?
@ -618,6 +756,11 @@ class RawConfigParser:
if not optname: if not optname:
e = self._handle_error(e, fpname, lineno, line) e = self._handle_error(e, fpname, lineno, line)
optname = self.optionxform(optname.rstrip()) optname = self.optionxform(optname.rstrip())
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 # This check is fine because the OPTCRE cannot
# match if it would set optval to None # match if it would set optval to None
if optval is not None: if optval is not None:
@ -692,8 +835,7 @@ class ConfigParser(RawConfigParser):
return self._interpolate(section, option, value, d) return self._interpolate(section, option, value, d)
def items(self, section, raw=False, vars=None): def items(self, section, raw=False, vars=None):
"""Return a list of tuples with (name, value) for each option """Return a list of (name, value) tuples for each option in a section.
in the section.
All % interpolations are expanded in the return values, based on the All % interpolations are expanded in the return values, based on the
defaults passed into the constructor, unless the optional argument defaults passed into the constructor, unless the optional argument
@ -799,7 +941,8 @@ class SafeConfigParser(ConfigParser):
else: else:
raise InterpolationSyntaxError( raise InterpolationSyntaxError(
option, section, option, section,
"'%%' must be followed by '%%' or '(', found: %r" % (rest,)) "'%%' must be followed by '%%' or '(', "
"found: %r" % (rest,))
def set(self, section, option, value=None): def set(self, section, option, value=None):
"""Set an option. Extend ConfigParser.set: check for string values.""" """Set an option. Extend ConfigParser.set: check for string values."""
@ -811,13 +954,11 @@ class SafeConfigParser(ConfigParser):
if self._optcre is self.OPTCRE or value: if self._optcre is self.OPTCRE or value:
if not isinstance(value, str): if not isinstance(value, str):
raise TypeError("option values must be strings") raise TypeError("option values must be strings")
# check for bad percent signs: # check for bad percent signs
# first, replace all "good" interpolations if value:
tmp_value = value.replace('%%', '') tmp_value = value.replace('%%', '') # escaped percent signs
tmp_value = self._interpvar_re.sub('', tmp_value) tmp_value = self._interpvar_re.sub('', tmp_value) # valid syntax
# then, check if there's a lone percent sign left if '%' in tmp_value:
percent_index = tmp_value.find('%') raise ValueError("invalid interpolation syntax in %r at "
if percent_index != -1: "position %d" % (value, tmp_value.find('%')))
raise ValueError("invalid interpolation syntax in %r at "
"position %d" % (value, percent_index))
ConfigParser.set(self, section, option, value) ConfigParser.set(self, section, option, value)

View File

@ -30,62 +30,28 @@ class CfgParserTestCaseClass(unittest.TestCase):
comment_prefixes = (';', '#') comment_prefixes = (';', '#')
empty_lines_in_values = True empty_lines_in_values = True
dict_type = configparser._default_dict dict_type = configparser._default_dict
strict = False
def newconfig(self, defaults=None): def newconfig(self, defaults=None):
arguments = dict( arguments = dict(
defaults=defaults,
allow_no_value=self.allow_no_value, allow_no_value=self.allow_no_value,
delimiters=self.delimiters, delimiters=self.delimiters,
comment_prefixes=self.comment_prefixes, comment_prefixes=self.comment_prefixes,
empty_lines_in_values=self.empty_lines_in_values, empty_lines_in_values=self.empty_lines_in_values,
dict_type=self.dict_type, dict_type=self.dict_type,
strict=self.strict,
) )
if defaults is None: return self.config_class(**arguments)
self.cf = self.config_class(**arguments)
else:
self.cf = self.config_class(defaults,
**arguments)
return self.cf
def fromstring(self, string, defaults=None): def fromstring(self, string, defaults=None):
cf = self.newconfig(defaults) cf = self.newconfig(defaults)
sio = io.StringIO(string) cf.read_string(string)
cf.readfp(sio)
return cf return cf
class BasicTestCase(CfgParserTestCaseClass): class BasicTestCase(CfgParserTestCaseClass):
def test_basic(self): def basic_test(self, cf):
config_string = """\
[Foo Bar]
foo{0[0]}bar
[Spacey Bar]
foo {0[0]} bar
[Spacey Bar From The Beginning]
foo {0[0]} bar
baz {0[0]} qwe
[Commented Bar]
foo{0[1]} bar {1[1]} comment
baz{0[0]}qwe {1[0]}another one
[Long Line]
foo{0[1]} this line is much, much longer than my editor
likes it.
[Section\\with$weird%characters[\t]
[Internationalized Stuff]
foo[bg]{0[1]} Bulgarian
foo{0[0]}Default
foo[en]{0[0]}English
foo[de]{0[0]}Deutsch
[Spaces]
key with spaces {0[1]} value
another with spaces {0[0]} splat!
""".format(self.delimiters, self.comment_prefixes)
if self.allow_no_value:
config_string += (
"[NoValue]\n"
"option-without-value\n"
)
cf = self.fromstring(config_string)
L = cf.sections() L = cf.sections()
L.sort() L.sort()
E = ['Commented Bar', E = ['Commented Bar',
@ -137,6 +103,125 @@ another with spaces {0[0]} splat!
eq(cf.get('Long Line', 'foo'), eq(cf.get('Long Line', 'foo'),
'this line is much, much longer than my editor\nlikes it.') 'this line is much, much longer than my editor\nlikes it.')
def test_basic(self):
config_string = """\
[Foo Bar]
foo{0[0]}bar
[Spacey Bar]
foo {0[0]} bar
[Spacey Bar From The Beginning]
foo {0[0]} bar
baz {0[0]} qwe
[Commented Bar]
foo{0[1]} bar {1[1]} comment
baz{0[0]}qwe {1[0]}another one
[Long Line]
foo{0[1]} this line is much, much longer than my editor
likes it.
[Section\\with$weird%characters[\t]
[Internationalized Stuff]
foo[bg]{0[1]} Bulgarian
foo{0[0]}Default
foo[en]{0[0]}English
foo[de]{0[0]}Deutsch
[Spaces]
key with spaces {0[1]} value
another with spaces {0[0]} splat!
""".format(self.delimiters, self.comment_prefixes)
if self.allow_no_value:
config_string += (
"[NoValue]\n"
"option-without-value\n"
)
cf = self.fromstring(config_string)
self.basic_test(cf)
if self.strict:
with self.assertRaises(configparser.DuplicateOptionError):
cf.read_string(textwrap.dedent("""\
[Duplicate Options Here]
option {0[0]} with a value
option {0[1]} with another value
""".format(self.delimiters)))
with self.assertRaises(configparser.DuplicateSectionError):
cf.read_string(textwrap.dedent("""\
[And Now For Something]
completely different {0[0]} True
[And Now For Something]
the larch {0[1]} 1
""".format(self.delimiters)))
else:
cf.read_string(textwrap.dedent("""\
[Duplicate Options Here]
option {0[0]} with a value
option {0[1]} with another value
""".format(self.delimiters)))
cf.read_string(textwrap.dedent("""\
[And Now For Something]
completely different {0[0]} True
[And Now For Something]
the larch {0[1]} 1
""".format(self.delimiters)))
def test_basic_from_dict(self):
config = {
"Foo Bar": {
"foo": "bar",
},
"Spacey Bar": {
"foo": "bar",
},
"Spacey Bar From The Beginning": {
"foo": "bar",
"baz": "qwe",
},
"Commented Bar": {
"foo": "bar",
"baz": "qwe",
},
"Long Line": {
"foo": "this line is much, much longer than my editor\nlikes "
"it.",
},
"Section\\with$weird%characters[\t": {
},
"Internationalized Stuff": {
"foo[bg]": "Bulgarian",
"foo": "Default",
"foo[en]": "English",
"foo[de]": "Deutsch",
},
"Spaces": {
"key with spaces": "value",
"another with spaces": "splat!",
}
}
if self.allow_no_value:
config.update({
"NoValue": {
"option-without-value": None,
}
})
cf = self.newconfig()
cf.read_dict(config)
self.basic_test(cf)
if self.strict:
with self.assertRaises(configparser.DuplicateOptionError):
cf.read_dict({
"Duplicate Options Here": {
'option': 'with a value',
'OPTION': 'with another value',
},
})
else:
cf.read_dict({
"Duplicate Options Here": {
'option': 'with a value',
'OPTION': 'with another value',
},
})
def test_case_sensitivity(self): def test_case_sensitivity(self):
cf = self.newconfig() cf = self.newconfig()
cf.add_section("A") cf.add_section("A")
@ -185,25 +270,25 @@ another with spaces {0[0]} splat!
"could not locate option, expecting case-insensitive defaults") "could not locate option, expecting case-insensitive defaults")
def test_parse_errors(self): def test_parse_errors(self):
self.newconfig() cf = self.newconfig()
self.parse_error(configparser.ParsingError, self.parse_error(cf, configparser.ParsingError,
"[Foo]\n" "[Foo]\n"
"{}val-without-opt-name\n".format(self.delimiters[0])) "{}val-without-opt-name\n".format(self.delimiters[0]))
self.parse_error(configparser.ParsingError, self.parse_error(cf, configparser.ParsingError,
"[Foo]\n" "[Foo]\n"
"{}val-without-opt-name\n".format(self.delimiters[1])) "{}val-without-opt-name\n".format(self.delimiters[1]))
e = self.parse_error(configparser.MissingSectionHeaderError, e = self.parse_error(cf, configparser.MissingSectionHeaderError,
"No Section!\n") "No Section!\n")
self.assertEqual(e.args, ('<???>', 1, "No Section!\n")) self.assertEqual(e.args, ('<???>', 1, "No Section!\n"))
if not self.allow_no_value: if not self.allow_no_value:
e = self.parse_error(configparser.ParsingError, e = self.parse_error(cf, configparser.ParsingError,
"[Foo]\n wrong-indent\n") "[Foo]\n wrong-indent\n")
self.assertEqual(e.args, ('<???>',)) self.assertEqual(e.args, ('<???>',))
def parse_error(self, exc, src): def parse_error(self, cf, exc, src):
sio = io.StringIO(src) sio = io.StringIO(src)
with self.assertRaises(exc) as cm: with self.assertRaises(exc) as cm:
self.cf.readfp(sio) cf.read_file(sio)
return cm.exception return cm.exception
def test_query_errors(self): def test_query_errors(self):
@ -217,15 +302,15 @@ another with spaces {0[0]} splat!
cf.options("Foo") cf.options("Foo")
with self.assertRaises(configparser.NoSectionError): with self.assertRaises(configparser.NoSectionError):
cf.set("foo", "bar", "value") cf.set("foo", "bar", "value")
e = self.get_error(configparser.NoSectionError, "foo", "bar") e = self.get_error(cf, configparser.NoSectionError, "foo", "bar")
self.assertEqual(e.args, ("foo",)) self.assertEqual(e.args, ("foo",))
cf.add_section("foo") cf.add_section("foo")
e = self.get_error(configparser.NoOptionError, "foo", "bar") e = self.get_error(cf, configparser.NoOptionError, "foo", "bar")
self.assertEqual(e.args, ("bar", "foo")) self.assertEqual(e.args, ("bar", "foo"))
def get_error(self, exc, section, option): def get_error(self, cf, exc, section, option):
try: try:
self.cf.get(section, option) cf.get(section, option)
except exc as e: except exc as e:
return e return e
else: else:
@ -262,7 +347,31 @@ another with spaces {0[0]} splat!
cf.add_section("Foo") cf.add_section("Foo")
with self.assertRaises(configparser.DuplicateSectionError) as cm: with self.assertRaises(configparser.DuplicateSectionError) as cm:
cf.add_section("Foo") cf.add_section("Foo")
self.assertEqual(cm.exception.args, ("Foo",)) e = cm.exception
self.assertEqual(str(e), "Section 'Foo' already exists")
self.assertEqual(e.args, ("Foo", None, None))
if self.strict:
with self.assertRaises(configparser.DuplicateSectionError) as cm:
cf.read_string(textwrap.dedent("""\
[Foo]
will this be added{equals}True
[Bar]
what about this{equals}True
[Foo]
oops{equals}this won't
""".format(equals=self.delimiters[0])), source='<foo-bar>')
e = cm.exception
self.assertEqual(str(e), "While reading from <foo-bar> [line 5]: "
"section 'Foo' already exists")
self.assertEqual(e.args, ("Foo", '<foo-bar>', 5))
with self.assertRaises(configparser.DuplicateOptionError) as cm:
cf.read_dict({'Bar': {'opt': 'val', 'OPT': 'is really `opt`'}})
e = cm.exception
self.assertEqual(str(e), "While reading from <dict>: option 'opt' "
"in section 'Bar' already exists")
self.assertEqual(e.args, ("Bar", "opt", "<dict>", None))
def test_write(self): def test_write(self):
config_string = ( config_string = (
@ -392,6 +501,11 @@ another with spaces {0[0]} splat!
self.assertEqual(L, expected) self.assertEqual(L, expected)
class StrictTestCase(BasicTestCase):
config_class = configparser.RawConfigParser
strict = True
class ConfigParserTestCase(BasicTestCase): class ConfigParserTestCase(BasicTestCase):
config_class = configparser.ConfigParser config_class = configparser.ConfigParser
@ -409,7 +523,7 @@ class ConfigParserTestCase(BasicTestCase):
"something with lots of interpolation (9 steps)") "something with lots of interpolation (9 steps)")
eq(cf.get("Foo", "bar10"), eq(cf.get("Foo", "bar10"),
"something with lots of interpolation (10 steps)") "something with lots of interpolation (10 steps)")
e = self.get_error(configparser.InterpolationDepthError, "Foo", "bar11") e = self.get_error(cf, configparser.InterpolationDepthError, "Foo", "bar11")
self.assertEqual(e.args, ("bar11", "Foo", rawval[self.config_class])) self.assertEqual(e.args, ("bar11", "Foo", rawval[self.config_class]))
def test_interpolation_missing_value(self): def test_interpolation_missing_value(self):
@ -417,8 +531,8 @@ class ConfigParserTestCase(BasicTestCase):
configparser.ConfigParser: '%(reference)s', configparser.ConfigParser: '%(reference)s',
configparser.SafeConfigParser: '', configparser.SafeConfigParser: '',
} }
self.get_interpolation_config() cf = self.get_interpolation_config()
e = self.get_error(configparser.InterpolationMissingOptionError, e = self.get_error(cf, configparser.InterpolationMissingOptionError,
"Interpolation Error", "name") "Interpolation Error", "name")
self.assertEqual(e.reference, "reference") self.assertEqual(e.reference, "reference")
self.assertEqual(e.section, "Interpolation Error") self.assertEqual(e.section, "Interpolation Error")
@ -482,7 +596,7 @@ class MultilineValuesTestCase(BasicTestCase):
# during performance updates in Python 3.2 # during performance updates in Python 3.2
cf_from_file = self.newconfig() cf_from_file = self.newconfig()
with open(support.TESTFN) as f: with open(support.TESTFN) as f:
cf_from_file.readfp(f) cf_from_file.read_file(f)
self.assertEqual(cf_from_file.get('section8', 'lovely_spam4'), self.assertEqual(cf_from_file.get('section8', 'lovely_spam4'),
self.wonderful_spam.replace('\t\n', '\n')) self.wonderful_spam.replace('\t\n', '\n'))
@ -645,15 +759,15 @@ class SortedTestCase(RawConfigParserTestCase):
dict_type = SortedDict dict_type = SortedDict
def test_sorted(self): def test_sorted(self):
self.fromstring("[b]\n" cf = self.fromstring("[b]\n"
"o4=1\n" "o4=1\n"
"o3=2\n" "o3=2\n"
"o2=3\n" "o2=3\n"
"o1=4\n" "o1=4\n"
"[a]\n" "[a]\n"
"k=v\n") "k=v\n")
output = io.StringIO() output = io.StringIO()
self.cf.write(output) cf.write(output)
self.assertEquals(output.getvalue(), self.assertEquals(output.getvalue(),
"[a]\n" "[a]\n"
"k = v\n\n" "k = v\n\n"
@ -697,6 +811,7 @@ def test_main():
SafeConfigParserTestCaseNoValue, SafeConfigParserTestCaseNoValue,
SafeConfigParserTestCaseTrickyFile, SafeConfigParserTestCaseTrickyFile,
SortedTestCase, SortedTestCase,
StrictTestCase,
CompatibleTestCase, CompatibleTestCase,
) )

View File

@ -67,6 +67,9 @@ Extensions
Library Library
------- -------
- Issue #9452: Add read_file, read_string, and read_dict to the configparser
API; new source attribute to exceptions.
- Issue #6231: Fix xml.etree.ElementInclude to include the tail of the - Issue #6231: Fix xml.etree.ElementInclude to include the tail of the
current node. current node.