issue #9452:
Add read_file, read_string, and read_dict to the configparser API; new source attribute to exceptions.
This commit is contained in:
parent
f14c263280
commit
a492362f9a
|
@ -27,8 +27,9 @@ customized by end users easily.
|
|||
the Windows Registry extended version of INI syntax.
|
||||
|
||||
A configuration file consists of sections, each led by a ``[section]`` header,
|
||||
followed by name/value entries separated by a specific string (``=`` or ``:`` by
|
||||
default). Note that leading whitespace is removed from values. Values can be
|
||||
followed by key/value entries separated by a specific string (``=`` or ``:`` by
|
||||
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
|
||||
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
|
||||
|
@ -101,7 +102,7 @@ that sorts its keys, the sections will be sorted on write-back, as will be the
|
|||
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
|
||||
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
|
||||
start whole line comments while only ``;`` can start inline comments.
|
||||
|
||||
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``.
|
||||
When *strict* is ``True`` (default: ``False``), the parser won't allow for
|
||||
any section or option duplicates while reading from a single source (file,
|
||||
string or dictionary), raising :exc:`DuplicateSectionError` or
|
||||
:exc:`DuplicateOptionError`. 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``.
|
||||
|
||||
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`.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
*delimiters*, *comment_prefixes*, *empty_lines_in_values* and
|
||||
*allow_no_value* were added.
|
||||
*allow_no_value*, *delimiters*, *comment_prefixes*, *strict* and
|
||||
*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
|
||||
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`.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
*delimiters*, *comment_prefixes*, *empty_lines_in_values* and
|
||||
*allow_no_value* were added.
|
||||
*allow_no_value*, *delimiters*, *comment_prefixes*, *strict* and
|
||||
*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
|
||||
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`.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
*delimiters*, *comment_prefixes*, *empty_lines_in_values* and
|
||||
*allow_no_value* were added.
|
||||
*allow_no_value*, *delimiters*, *comment_prefixes*,
|
||||
*strict* and *empty_lines_in_values* were added.
|
||||
|
||||
|
||||
.. exception:: Error
|
||||
|
@ -191,12 +195,26 @@ keys within each section.
|
|||
.. exception:: DuplicateSectionError
|
||||
|
||||
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 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
|
||||
|
@ -233,6 +251,9 @@ keys within each section.
|
|||
|
||||
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
|
||||
|
||||
|
@ -315,15 +336,41 @@ RawConfigParser Objects
|
|||
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
|
||||
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
|
||||
for *filename*; the default is ``<???>``.
|
||||
Optional argument *source* specifies the name of the file being read. It not
|
||||
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)
|
||||
|
||||
|
@ -408,6 +455,10 @@ RawConfigParser Objects
|
|||
Note that when reading configuration files, whitespace around the
|
||||
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:
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ ConfigParser -- responsible for parsing a list of
|
|||
|
||||
__init__(defaults=None, dict_type=_default_dict,
|
||||
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
|
||||
dictionary or intrinsic defaults. The keys must be strings, the values
|
||||
must be appropriate for %()s string interpolation. Note that `__name__'
|
||||
|
@ -42,6 +42,10 @@ ConfigParser -- responsible for parsing a list of
|
|||
When `comment_prefixes' is given, it will be used as the set of
|
||||
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
|
||||
marks the end of an option. Otherwise, internal empty lines of
|
||||
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
|
||||
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.
|
||||
The filename defaults to fp.name; it is only used in error
|
||||
messages (if fp has no `name' attribute, the string `<???>' is used).
|
||||
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.
|
||||
|
||||
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)
|
||||
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
|
||||
_default_dict = dict
|
||||
|
||||
import io
|
||||
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",
|
||||
|
@ -147,8 +162,8 @@ class Error(Exception):
|
|||
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=''):
|
||||
|
@ -171,12 +186,56 @@ class NoSectionError(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):
|
||||
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.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):
|
||||
|
@ -216,8 +275,12 @@ class InterpolationMissingOptionError(InterpolationError):
|
|||
|
||||
|
||||
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 only for SafeConfigParser
|
||||
instances when the source text into which substitutions are made
|
||||
does not conform to the required syntax.
|
||||
"""
|
||||
|
||||
|
||||
class InterpolationDepthError(InterpolationError):
|
||||
|
@ -236,11 +299,40 @@ class InterpolationDepthError(InterpolationError):
|
|||
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 = (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):
|
||||
self.errors.append((lineno, line))
|
||||
|
@ -255,7 +347,7 @@ 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)
|
||||
|
@ -302,8 +394,9 @@ class RawConfigParser:
|
|||
_COMPATIBLE = object()
|
||||
|
||||
def __init__(self, defaults=None, dict_type=_default_dict,
|
||||
delimiters=('=', ':'), comment_prefixes=_COMPATIBLE,
|
||||
empty_lines_in_values=True, allow_no_value=False):
|
||||
allow_no_value=False, *, delimiters=('=', ':'),
|
||||
comment_prefixes=_COMPATIBLE, strict=False,
|
||||
empty_lines_in_values=True):
|
||||
self._dict = dict_type
|
||||
self._sections = self._dict()
|
||||
self._defaults = self._dict()
|
||||
|
@ -314,12 +407,12 @@ class RawConfigParser:
|
|||
if delimiters == ('=', ':'):
|
||||
self._optcre = self.OPTCRE_NV if allow_no_value else self.OPTCRE
|
||||
else:
|
||||
delim = "|".join(re.escape(d) for d in delimiters)
|
||||
d = "|".join(re.escape(d) for d in delimiters)
|
||||
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)
|
||||
else:
|
||||
self._optcre = re.compile(self._OPT_TMPL.format(delim=delim),
|
||||
self._optcre = re.compile(self._OPT_TMPL.format(delim=d),
|
||||
re.VERBOSE)
|
||||
if comment_prefixes is self._COMPATIBLE:
|
||||
self._startonly_comment_prefixes = ('#',)
|
||||
|
@ -327,6 +420,7 @@ class RawConfigParser:
|
|||
else:
|
||||
self._startonly_comment_prefixes = ()
|
||||
self._comment_prefixes = tuple(comment_prefixes or ())
|
||||
self._strict = strict
|
||||
self._empty_lines_in_values = empty_lines_in_values
|
||||
|
||||
def defaults(self):
|
||||
|
@ -394,20 +488,59 @@ class RawConfigParser:
|
|||
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 have a `readline' method. 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
|
||||
srouce = f.name
|
||||
except AttributeError:
|
||||
filename = '<???>'
|
||||
self._read(fp, filename)
|
||||
source = '<???>'
|
||||
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):
|
||||
opt = self.optionxform(option)
|
||||
|
@ -461,7 +594,6 @@ 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
|
||||
|
@ -474,7 +606,6 @@ class RawConfigParser:
|
|||
|
||||
def set(self, section, option, value=None):
|
||||
"""Set an option."""
|
||||
|
||||
if not section or section == DEFAULTSECT:
|
||||
sectdict = self._defaults
|
||||
else:
|
||||
|
@ -538,21 +669,23 @@ class RawConfigParser:
|
|||
def _read(self, fp, fpname):
|
||||
"""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
|
||||
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.
|
||||
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
|
||||
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.
|
||||
"""
|
||||
elements_added = set()
|
||||
cursect = None # None, or a dictionary
|
||||
sectname = None
|
||||
optname = None
|
||||
lineno = 0
|
||||
indent_level = 0
|
||||
|
@ -598,13 +731,18 @@ class RawConfigParser:
|
|||
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]
|
||||
elements_added.add(sectname)
|
||||
elif sectname == DEFAULTSECT:
|
||||
cursect = self._defaults
|
||||
else:
|
||||
cursect = self._dict()
|
||||
cursect['__name__'] = sectname
|
||||
self._sections[sectname] = cursect
|
||||
elements_added.add(sectname)
|
||||
# So sections can't start with a continuation line
|
||||
optname = None
|
||||
# no section header in the file?
|
||||
|
@ -618,6 +756,11 @@ class RawConfigParser:
|
|||
if not optname:
|
||||
e = self._handle_error(e, fpname, lineno, line)
|
||||
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
|
||||
# match if it would set optval to None
|
||||
if optval is not None:
|
||||
|
@ -692,8 +835,7 @@ class ConfigParser(RawConfigParser):
|
|||
return self._interpolate(section, option, value, d)
|
||||
|
||||
def items(self, section, raw=False, vars=None):
|
||||
"""Return a list of tuples with (name, value) for each option
|
||||
in the section.
|
||||
"""Return a list of (name, value) tuples for each option in a section.
|
||||
|
||||
All % interpolations are expanded in the return values, based on the
|
||||
defaults passed into the constructor, unless the optional argument
|
||||
|
@ -799,7 +941,8 @@ class SafeConfigParser(ConfigParser):
|
|||
else:
|
||||
raise InterpolationSyntaxError(
|
||||
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):
|
||||
"""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 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:
|
||||
# check for bad percent signs
|
||||
if value:
|
||||
tmp_value = value.replace('%%', '') # escaped percent signs
|
||||
tmp_value = self._interpvar_re.sub('', tmp_value) # valid syntax
|
||||
if '%' in tmp_value:
|
||||
raise ValueError("invalid interpolation syntax in %r at "
|
||||
"position %d" % (value, percent_index))
|
||||
"position %d" % (value, tmp_value.find('%')))
|
||||
ConfigParser.set(self, section, option, value)
|
||||
|
|
|
@ -30,62 +30,28 @@ class CfgParserTestCaseClass(unittest.TestCase):
|
|||
comment_prefixes = (';', '#')
|
||||
empty_lines_in_values = True
|
||||
dict_type = configparser._default_dict
|
||||
strict = False
|
||||
|
||||
def newconfig(self, defaults=None):
|
||||
arguments = dict(
|
||||
defaults=defaults,
|
||||
allow_no_value=self.allow_no_value,
|
||||
delimiters=self.delimiters,
|
||||
comment_prefixes=self.comment_prefixes,
|
||||
empty_lines_in_values=self.empty_lines_in_values,
|
||||
dict_type=self.dict_type,
|
||||
strict=self.strict,
|
||||
)
|
||||
if defaults is None:
|
||||
self.cf = self.config_class(**arguments)
|
||||
else:
|
||||
self.cf = self.config_class(defaults,
|
||||
**arguments)
|
||||
return self.cf
|
||||
return self.config_class(**arguments)
|
||||
|
||||
def fromstring(self, string, defaults=None):
|
||||
cf = self.newconfig(defaults)
|
||||
sio = io.StringIO(string)
|
||||
cf.readfp(sio)
|
||||
cf.read_string(string)
|
||||
return cf
|
||||
|
||||
class BasicTestCase(CfgParserTestCaseClass):
|
||||
|
||||
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)
|
||||
def basic_test(self, cf):
|
||||
L = cf.sections()
|
||||
L.sort()
|
||||
E = ['Commented Bar',
|
||||
|
@ -137,6 +103,125 @@ another with spaces {0[0]} splat!
|
|||
eq(cf.get('Long Line', 'foo'),
|
||||
'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):
|
||||
cf = self.newconfig()
|
||||
cf.add_section("A")
|
||||
|
@ -185,25 +270,25 @@ another with spaces {0[0]} splat!
|
|||
"could not locate option, expecting case-insensitive defaults")
|
||||
|
||||
def test_parse_errors(self):
|
||||
self.newconfig()
|
||||
self.parse_error(configparser.ParsingError,
|
||||
cf = self.newconfig()
|
||||
self.parse_error(cf, configparser.ParsingError,
|
||||
"[Foo]\n"
|
||||
"{}val-without-opt-name\n".format(self.delimiters[0]))
|
||||
self.parse_error(configparser.ParsingError,
|
||||
self.parse_error(cf, configparser.ParsingError,
|
||||
"[Foo]\n"
|
||||
"{}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")
|
||||
self.assertEqual(e.args, ('<???>', 1, "No Section!\n"))
|
||||
if not self.allow_no_value:
|
||||
e = self.parse_error(configparser.ParsingError,
|
||||
e = self.parse_error(cf, configparser.ParsingError,
|
||||
"[Foo]\n wrong-indent\n")
|
||||
self.assertEqual(e.args, ('<???>',))
|
||||
|
||||
def parse_error(self, exc, src):
|
||||
def parse_error(self, cf, exc, src):
|
||||
sio = io.StringIO(src)
|
||||
with self.assertRaises(exc) as cm:
|
||||
self.cf.readfp(sio)
|
||||
cf.read_file(sio)
|
||||
return cm.exception
|
||||
|
||||
def test_query_errors(self):
|
||||
|
@ -217,15 +302,15 @@ another with spaces {0[0]} splat!
|
|||
cf.options("Foo")
|
||||
with self.assertRaises(configparser.NoSectionError):
|
||||
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",))
|
||||
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"))
|
||||
|
||||
def get_error(self, exc, section, option):
|
||||
def get_error(self, cf, exc, section, option):
|
||||
try:
|
||||
self.cf.get(section, option)
|
||||
cf.get(section, option)
|
||||
except exc as e:
|
||||
return e
|
||||
else:
|
||||
|
@ -262,7 +347,31 @@ another with spaces {0[0]} splat!
|
|||
cf.add_section("Foo")
|
||||
with self.assertRaises(configparser.DuplicateSectionError) as cm:
|
||||
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):
|
||||
config_string = (
|
||||
|
@ -392,6 +501,11 @@ another with spaces {0[0]} splat!
|
|||
self.assertEqual(L, expected)
|
||||
|
||||
|
||||
class StrictTestCase(BasicTestCase):
|
||||
config_class = configparser.RawConfigParser
|
||||
strict = True
|
||||
|
||||
|
||||
class ConfigParserTestCase(BasicTestCase):
|
||||
config_class = configparser.ConfigParser
|
||||
|
||||
|
@ -409,7 +523,7 @@ class ConfigParserTestCase(BasicTestCase):
|
|||
"something with lots of interpolation (9 steps)")
|
||||
eq(cf.get("Foo", "bar10"),
|
||||
"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]))
|
||||
|
||||
def test_interpolation_missing_value(self):
|
||||
|
@ -417,8 +531,8 @@ class ConfigParserTestCase(BasicTestCase):
|
|||
configparser.ConfigParser: '%(reference)s',
|
||||
configparser.SafeConfigParser: '',
|
||||
}
|
||||
self.get_interpolation_config()
|
||||
e = self.get_error(configparser.InterpolationMissingOptionError,
|
||||
cf = self.get_interpolation_config()
|
||||
e = self.get_error(cf, configparser.InterpolationMissingOptionError,
|
||||
"Interpolation Error", "name")
|
||||
self.assertEqual(e.reference, "reference")
|
||||
self.assertEqual(e.section, "Interpolation Error")
|
||||
|
@ -482,7 +596,7 @@ class MultilineValuesTestCase(BasicTestCase):
|
|||
# during performance updates in Python 3.2
|
||||
cf_from_file = self.newconfig()
|
||||
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.wonderful_spam.replace('\t\n', '\n'))
|
||||
|
||||
|
@ -645,7 +759,7 @@ class SortedTestCase(RawConfigParserTestCase):
|
|||
dict_type = SortedDict
|
||||
|
||||
def test_sorted(self):
|
||||
self.fromstring("[b]\n"
|
||||
cf = self.fromstring("[b]\n"
|
||||
"o4=1\n"
|
||||
"o3=2\n"
|
||||
"o2=3\n"
|
||||
|
@ -653,7 +767,7 @@ class SortedTestCase(RawConfigParserTestCase):
|
|||
"[a]\n"
|
||||
"k=v\n")
|
||||
output = io.StringIO()
|
||||
self.cf.write(output)
|
||||
cf.write(output)
|
||||
self.assertEquals(output.getvalue(),
|
||||
"[a]\n"
|
||||
"k = v\n\n"
|
||||
|
@ -697,6 +811,7 @@ def test_main():
|
|||
SafeConfigParserTestCaseNoValue,
|
||||
SafeConfigParserTestCaseTrickyFile,
|
||||
SortedTestCase,
|
||||
StrictTestCase,
|
||||
CompatibleTestCase,
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue