summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorFred Drake <fdrake@acm.org>2010-08-09 12:52:45 (GMT)
committerFred Drake <fdrake@acm.org>2010-08-09 12:52:45 (GMT)
commita492362f9a2a44e411147fd7b2886466bb0bb17f (patch)
tree0e150dd20d8c8add5b3282bbac9efe16b8696e21 /Lib
parentf14c2632806ec19b0d58c2c1f721c6a31b535209 (diff)
downloadcpython-a492362f9a2a44e411147fd7b2886466bb0bb17f.zip
cpython-a492362f9a2a44e411147fd7b2886466bb0bb17f.tar.gz
cpython-a492362f9a2a44e411147fd7b2886466bb0bb17f.tar.bz2
issue #9452:
Add read_file, read_string, and read_dict to the configparser API; new source attribute to exceptions.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/configparser.py253
-rw-r--r--Lib/test/test_cfgparser.py243
2 files changed, 376 insertions, 120 deletions
diff --git a/Lib/configparser.py b/Lib/configparser.py
index 6e38f26..eb29b02 100644
--- a/Lib/configparser.py
+++ b/Lib/configparser.py
@@ -26,10 +26,10 @@ 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__'
+ 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
@@ -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."""
-
- def __init__(self, section):
- Error.__init__(self, "Section %r already exists" % section)
+ """Raised when a section is repeated in an input source.
+
+ 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:
- raise ValueError("invalid interpolation syntax in %r at "
- "position %d" % (value, percent_index))
+ # check for bad percent signs
+ if value:
+ tmp_value = value.replace('%%', '') # escaped percent signs
+ tmp_value = self._interpvar_re.sub('', tmp_value) # valid syntax
+ if '%' in tmp_value:
+ raise ValueError("invalid interpolation syntax in %r at "
+ "position %d" % (value, tmp_value.find('%')))
ConfigParser.set(self, section, option, value)
diff --git a/Lib/test/test_cfgparser.py b/Lib/test/test_cfgparser.py
index e54ccfe..f43d1d7 100644
--- a/Lib/test/test_cfgparser.py
+++ b/Lib/test/test_cfgparser.py
@@ -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,15 +759,15 @@ class SortedTestCase(RawConfigParserTestCase):
dict_type = SortedDict
def test_sorted(self):
- self.fromstring("[b]\n"
- "o4=1\n"
- "o3=2\n"
- "o2=3\n"
- "o1=4\n"
- "[a]\n"
- "k=v\n")
+ cf = self.fromstring("[b]\n"
+ "o4=1\n"
+ "o3=2\n"
+ "o2=3\n"
+ "o1=4\n"
+ "[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,
)