issue #9452:
Add read_file, read_string, and read_dict to the configparser API;
new source attribute to exceptions.
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 @@
 
     __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 @@
         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 @@
         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 @@
     # 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 @@
         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 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 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 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 @@
             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 @@
     _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 @@
         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 @@
         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 @@
             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 @@
 
     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 @@
 
     def set(self, section, option, value=None):
         """Set an option."""
-
         if not section or section == DEFAULTSECT:
             sectdict = self._defaults
         else:
@@ -538,21 +669,23 @@
     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 @@
                 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 @@
                         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 @@
             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 @@
             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 @@
         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 @@
     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 @@
         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 @@
             "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 @@
             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 @@
         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 @@
         self.assertEqual(L, expected)
 
 
+class StrictTestCase(BasicTestCase):
+    config_class = configparser.RawConfigParser
+    strict = True
+
+
 class ConfigParserTestCase(BasicTestCase):
     config_class = configparser.ConfigParser
 
@@ -409,7 +523,7 @@
            "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 @@
             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 @@
         # 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 @@
     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 @@
         SafeConfigParserTestCaseNoValue,
         SafeConfigParserTestCaseTrickyFile,
         SortedTestCase,
+        StrictTestCase,
         CompatibleTestCase,
         )