[Python-checkins] r87350 - in python/branches/py3k: Doc/library/configparser.rst Lib/configparser.py Lib/test/test_cfgparser.py Misc/NEWS

lukasz.langa python-checkins at python.org
Fri Dec 17 22:56:32 CET 2010


Author: lukasz.langa
Date: Fri Dec 17 22:56:32 2010
New Revision: 87350

Log:
100% test coverage, better mapping protocol compatibility, some minor bugfixes



Modified:
   python/branches/py3k/Doc/library/configparser.rst
   python/branches/py3k/Lib/configparser.py
   python/branches/py3k/Lib/test/test_cfgparser.py
   python/branches/py3k/Misc/NEWS

Modified: python/branches/py3k/Doc/library/configparser.rst
==============================================================================
--- python/branches/py3k/Doc/library/configparser.rst	(original)
+++ python/branches/py3k/Doc/library/configparser.rst	Fri Dec 17 22:56:32 2010
@@ -391,17 +391,20 @@
 
 * Trying to delete the ``DEFAULTSECT`` raises ``ValueError``.
 
-* There are two parser-level methods in the legacy API that hide the dictionary
-  interface and are incompatible:
-
-  * ``parser.get(section, option, **kwargs)`` - the second argument is **not** a
-    fallback value
-
-  * ``parser.items(section)`` - this returns a list of *option*, *value* pairs
-    for a specified ``section``
+* ``parser.get(section, option, **kwargs)`` - the second argument is **not**
+  a fallback value. Note however that the section-level ``get()`` methods are
+  compatible both with the mapping protocol and the classic configparser API.
+
+* ``parser.items()`` is compatible with the mapping protocol (returns a list of
+  *section_name*, *section_proxy* pairs including the DEFAULTSECT).  However,
+  this method can also be invoked with arguments: ``parser.items(section, raw,
+  vars)``. The latter call returns a list of *option*, *value* pairs for
+  a specified ``section``, with all interpolations expanded (unless
+  ``raw=True`` is provided).
 
 The mapping protocol is implemented on top of the existing legacy API so that
-subclassing the original interface makes the mappings work as expected as well.
+subclasses overriding the original interface still should have mappings working
+as expected.
 
 
 Customizing Parser Behaviour
@@ -906,7 +909,8 @@
    .. method:: has_option(section, option)
 
       If the given *section* exists, and contains the given *option*, return
-      :const:`True`; otherwise return :const:`False`.
+      :const:`True`; otherwise return :const:`False`. If the specified
+      *section* is :const:`None` or an empty string, DEFAULT is assumed.
 
 
    .. method:: read(filenames, encoding=None)
@@ -964,14 +968,17 @@
 
    .. method:: 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.  Values are automatically converted to strings.
+      Load configuration from any object that provides a dict-like ``items()``
+      method.  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.
+      Values are automatically converted to strings.
 
       Optional argument *source* specifies a context-specific name of the
       dictionary passed.  If not given, ``<dict>`` is used.
 
+      This method can be used to copy state between parsers.
+
       .. versionadded:: 3.2
 
 
@@ -1019,10 +1026,13 @@
       *fallback*.
 
 
-   .. method:: items(section, raw=False, vars=None)
+   .. method:: items([section], raw=False, vars=None)
+
+      When *section* is not given, return a list of *section_name*,
+      *section_proxy* pairs, including DEFAULTSECT.
 
-      Return a list of *name*, *value* pairs for the options in the given
-      *section*.  Optional arguments have the same meaning as for the
+      Otherwise, return a list of *name*, *value* pairs for the options in the
+      given *section*.  Optional arguments have the same meaning as for the
       :meth:`get` method.
 
 

Modified: python/branches/py3k/Lib/configparser.py
==============================================================================
--- python/branches/py3k/Lib/configparser.py	(original)
+++ python/branches/py3k/Lib/configparser.py	Fri Dec 17 22:56:32 2010
@@ -98,8 +98,10 @@
         insensitively defined as 0, false, no, off for False, and 1, true,
         yes, on for True).  Returns False or True.
 
-    items(section, raw=False, vars=None)
-        Return a list of tuples with (name, value) for each option
+    items(section=_UNSET, raw=False, vars=None)
+        If section is given, return a list of tuples with (section_name,
+        section_proxy) for each section, including DEFAULTSECT. Otherwise,
+        return a list of tuples with (name, value) for each option
         in the section.
 
     remove_section(section)
@@ -495,9 +497,9 @@
                         raise InterpolationSyntaxError(
                             option, section,
                             "More than one ':' found: %r" % (rest,))
-                except KeyError:
+                except (KeyError, NoSectionError, NoOptionError):
                     raise InterpolationMissingOptionError(
-                        option, section, rest, var)
+                        option, section, rest, ":".join(path))
                 if "$" in v:
                     self._interpolate_some(parser, opt, accum, v, sect,
                                            dict(parser.items(sect, raw=True)),
@@ -730,7 +732,7 @@
             except (DuplicateSectionError, ValueError):
                 if self._strict and section in elements_added:
                     raise
-                elements_added.add(section)
+            elements_added.add(section)
             for key, value in keys.items():
                 key = self.optionxform(str(key))
                 if value is not None:
@@ -820,7 +822,7 @@
             else:
                 return fallback
 
-    def items(self, section, raw=False, vars=None):
+    def items(self, section=_UNSET, raw=False, vars=None):
         """Return a list of (name, value) tuples for each option in a section.
 
         All % interpolations are expanded in the return values, based on the
@@ -831,6 +833,8 @@
 
         The section DEFAULT is special.
         """
+        if section is _UNSET:
+            return super().items()
         d = self._defaults.copy()
         try:
             d.update(self._sections[section])
@@ -851,7 +855,9 @@
         return optionstr.lower()
 
     def has_option(self, section, option):
-        """Check for the existence of a given option in a given section."""
+        """Check for the existence of a given option in a given section.
+        If the specified `section' is None or an empty string, DEFAULT is
+        assumed. If the specified `section' does not exist, returns False."""
         if not section or section == self.default_section:
             option = self.optionxform(option)
             return option in self._defaults
@@ -1059,9 +1065,6 @@
                         # match if it would set optval to None
                         if optval is not None:
                             optval = optval.strip()
-                            # allow empty values
-                            if optval == '""':
-                                optval = ''
                             cursect[optname] = [optval]
                         else:
                             # valueless option handling
@@ -1196,21 +1199,24 @@
         return self._parser.set(self._name, key, value)
 
     def __delitem__(self, key):
-        if not self._parser.has_option(self._name, key):
+        if not (self._parser.has_option(self._name, key) and
+                self._parser.remove_option(self._name, key)):
             raise KeyError(key)
-        return self._parser.remove_option(self._name, key)
 
     def __contains__(self, key):
         return self._parser.has_option(self._name, key)
 
     def __len__(self):
-        # XXX weak performance
-        return len(self._parser.options(self._name))
+        return len(self._options())
 
     def __iter__(self):
-        # XXX weak performance
-        # XXX does not break when underlying container state changed
-        return self._parser.options(self._name).__iter__()
+        return self._options().__iter__()
+
+    def _options(self):
+        if self._name != self._parser.default_section:
+            return self._parser.options(self._name)
+        else:
+            return self._parser.defaults()
 
     def get(self, option, fallback=None, *, raw=False, vars=None):
         return self._parser.get(self._name, option, raw=raw, vars=vars,

Modified: python/branches/py3k/Lib/test/test_cfgparser.py
==============================================================================
--- python/branches/py3k/Lib/test/test_cfgparser.py	(original)
+++ python/branches/py3k/Lib/test/test_cfgparser.py	Fri Dec 17 22:56:32 2010
@@ -5,6 +5,7 @@
 import sys
 import textwrap
 import unittest
+import warnings
 
 from test import support
 
@@ -74,12 +75,16 @@
         if self.allow_no_value:
             E.append('NoValue')
         E.sort()
+        F = [('baz', 'qwe'), ('foo', 'bar3')]
 
         # API access
         L = cf.sections()
         L.sort()
         eq = self.assertEqual
         eq(L, E)
+        L = cf.items('Spacey Bar From The Beginning')
+        L.sort()
+        eq(L, F)
 
         # mapping access
         L = [section for section in cf]
@@ -87,6 +92,15 @@
         E.append(self.default_section)
         E.sort()
         eq(L, E)
+        L = cf['Spacey Bar From The Beginning'].items()
+        L = sorted(list(L))
+        eq(L, F)
+        L = cf.items()
+        L = sorted(list(L))
+        self.assertEqual(len(L), len(E))
+        for name, section in L:
+            eq(name, section.name)
+        eq(cf.defaults(), cf[self.default_section])
 
         # The use of spaces in the section names serves as a
         # regression test for SourceForge bug #583248:
@@ -124,15 +138,21 @@
         eq(cf.getint('Types', 'int', fallback=18), 42)
         eq(cf.getint('Types', 'no-such-int', fallback=18), 18)
         eq(cf.getint('Types', 'no-such-int', fallback="18"), "18") # sic!
+        with self.assertRaises(configparser.NoOptionError):
+            cf.getint('Types', 'no-such-int')
         self.assertAlmostEqual(cf.getfloat('Types', 'float',
                                            fallback=0.0), 0.44)
         self.assertAlmostEqual(cf.getfloat('Types', 'no-such-float',
                                            fallback=0.0), 0.0)
         eq(cf.getfloat('Types', 'no-such-float', fallback="0.0"), "0.0") # sic!
+        with self.assertRaises(configparser.NoOptionError):
+            cf.getfloat('Types', 'no-such-float')
         eq(cf.getboolean('Types', 'boolean', fallback=True), False)
         eq(cf.getboolean('Types', 'no-such-boolean', fallback="yes"),
            "yes") # sic!
         eq(cf.getboolean('Types', 'no-such-boolean', fallback=True), True)
+        with self.assertRaises(configparser.NoOptionError):
+            cf.getboolean('Types', 'no-such-boolean')
         eq(cf.getboolean('No Such Types', 'boolean', fallback=True), True)
         if self.allow_no_value:
             eq(cf.get('NoValue', 'option-without-value', fallback=False), None)
@@ -171,6 +191,7 @@
             cf['No Such Foo Bar'].get('foo', fallback='baz')
         eq(cf['Foo Bar'].get('no-such-foo', 'baz'), 'baz')
         eq(cf['Foo Bar'].get('no-such-foo', fallback='baz'), 'baz')
+        eq(cf['Foo Bar'].get('no-such-foo'), None)
         eq(cf['Spacey Bar'].get('foo', None), 'bar2')
         eq(cf['Spacey Bar'].get('foo', fallback=None), 'bar2')
         with self.assertRaises(KeyError):
@@ -181,6 +202,7 @@
         eq(cf['Types'].getint('no-such-int', fallback=18), 18)
         eq(cf['Types'].getint('no-such-int', "18"), "18") # sic!
         eq(cf['Types'].getint('no-such-int', fallback="18"), "18") # sic!
+        eq(cf['Types'].getint('no-such-int'), None)
         self.assertAlmostEqual(cf['Types'].getfloat('float', 0.0), 0.44)
         self.assertAlmostEqual(cf['Types'].getfloat('float',
                                                     fallback=0.0), 0.44)
@@ -189,6 +211,7 @@
                                                     fallback=0.0), 0.0)
         eq(cf['Types'].getfloat('no-such-float', "0.0"), "0.0") # sic!
         eq(cf['Types'].getfloat('no-such-float', fallback="0.0"), "0.0") # sic!
+        eq(cf['Types'].getfloat('no-such-float'), None)
         eq(cf['Types'].getboolean('boolean', True), False)
         eq(cf['Types'].getboolean('boolean', fallback=True), False)
         eq(cf['Types'].getboolean('no-such-boolean', "yes"), "yes") # sic!
@@ -196,6 +219,7 @@
            "yes") # sic!
         eq(cf['Types'].getboolean('no-such-boolean', True), True)
         eq(cf['Types'].getboolean('no-such-boolean', fallback=True), True)
+        eq(cf['Types'].getboolean('no-such-boolean'), None)
         if self.allow_no_value:
             eq(cf['NoValue'].get('option-without-value', False), None)
             eq(cf['NoValue'].get('option-without-value', fallback=False), None)
@@ -203,10 +227,17 @@
             eq(cf['NoValue'].get('no-such-option-without-value',
                       fallback=False), False)
 
-        # Make sure the right things happen for remove_option();
-        # added to include check for SourceForge bug #123324:
+        # Make sure the right things happen for remove_section() and
+        # remove_option(); added to include check for SourceForge bug #123324.
 
-        # API acceess
+        cf[self.default_section]['this_value'] = '1'
+        cf[self.default_section]['that_value'] = '2'
+
+        # API access
+        self.assertTrue(cf.remove_section('Spaces'))
+        self.assertFalse(cf.has_option('Spaces', 'key with spaces'))
+        self.assertFalse(cf.remove_section('Spaces'))
+        self.assertFalse(cf.remove_section(self.default_section))
         self.assertTrue(cf.remove_option('Foo Bar', 'foo'),
                         "remove_option() failed to report existence of option")
         self.assertFalse(cf.has_option('Foo Bar', 'foo'),
@@ -214,6 +245,11 @@
         self.assertFalse(cf.remove_option('Foo Bar', 'foo'),
                     "remove_option() failed to report non-existence of option"
                     " that was removed")
+        self.assertTrue(cf.has_option('Foo Bar', 'this_value'))
+        self.assertFalse(cf.remove_option('Foo Bar', 'this_value'))
+        self.assertTrue(cf.remove_option(self.default_section, 'this_value'))
+        self.assertFalse(cf.has_option('Foo Bar', 'this_value'))
+        self.assertFalse(cf.remove_option(self.default_section, 'this_value'))
 
         with self.assertRaises(configparser.NoSectionError) as cm:
             cf.remove_option('No Such Section', 'foo')
@@ -223,13 +259,29 @@
            'this line is much, much longer than my editor\nlikes it.')
 
         # mapping access
+        del cf['Types']
+        self.assertFalse('Types' in cf)
+        with self.assertRaises(KeyError):
+            del cf['Types']
+        with self.assertRaises(ValueError):
+            del cf[self.default_section]
         del cf['Spacey Bar']['foo']
         self.assertFalse('foo' in cf['Spacey Bar'])
         with self.assertRaises(KeyError):
             del cf['Spacey Bar']['foo']
+        self.assertTrue('that_value' in cf['Spacey Bar'])
+        with self.assertRaises(KeyError):
+            del cf['Spacey Bar']['that_value']
+        del cf[self.default_section]['that_value']
+        self.assertFalse('that_value' in cf['Spacey Bar'])
+        with self.assertRaises(KeyError):
+            del cf[self.default_section]['that_value']
         with self.assertRaises(KeyError):
             del cf['No Such Section']['foo']
 
+        # Don't add new asserts below in this method as most of the options
+        # and sections are now removed.
+
     def test_basic(self):
         config_string = """\
 [Foo Bar]
@@ -344,6 +396,11 @@
         cf.read_dict(config)
         self.basic_test(cf)
         if self.strict:
+            with self.assertRaises(configparser.DuplicateSectionError):
+                cf.read_dict({
+                    '1': {'key': 'value'},
+                    1: {'key2': 'value2'},
+                })
             with self.assertRaises(configparser.DuplicateOptionError):
                 cf.read_dict({
                     "Duplicate Options Here": {
@@ -353,13 +410,16 @@
                 })
         else:
             cf.read_dict({
+                'section': {'key': 'value'},
+                'SECTION': {'key2': 'value2'},
+            })
+            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")
@@ -377,6 +437,7 @@
             # section names are case-sensitive
             cf.set("b", "A", "value")
         self.assertTrue(cf.has_option("a", "b"))
+        self.assertFalse(cf.has_option("b", "b"))
         cf.set("A", "A-B", "A-B value")
         for opt in ("a-b", "A-b", "a-B", "A-B"):
             self.assertTrue(
@@ -593,32 +654,36 @@
             )
 
         cf = self.fromstring(config_string)
-        output = io.StringIO()
-        cf.write(output)
-        expect_string = (
-            "[{default_section}]\n"
-            "foo {equals} another very\n"
-            "\tlong line\n"
-            "\n"
-            "[Long Line]\n"
-            "foo {equals} this line is much, much longer than my editor\n"
-            "\tlikes it.\n"
-            "\n"
-            "[Long Line - With Comments!]\n"
-            "test {equals} we\n"
-            "\talso\n"
-            "\tcomments\n"
-            "\tmultiline\n"
-            "\n".format(equals=self.delimiters[0],
-                        default_section=self.default_section)
-            )
-        if self.allow_no_value:
-            expect_string += (
-                "[Valueless]\n"
-                "option-without-value\n"
+        for space_around_delimiters in (True, False):
+            output = io.StringIO()
+            cf.write(output, space_around_delimiters=space_around_delimiters)
+            delimiter = self.delimiters[0]
+            if space_around_delimiters:
+                delimiter = " {} ".format(delimiter)
+            expect_string = (
+                "[{default_section}]\n"
+                "foo{equals}another very\n"
+                "\tlong line\n"
                 "\n"
+                "[Long Line]\n"
+                "foo{equals}this line is much, much longer than my editor\n"
+                "\tlikes it.\n"
+                "\n"
+                "[Long Line - With Comments!]\n"
+                "test{equals}we\n"
+                "\talso\n"
+                "\tcomments\n"
+                "\tmultiline\n"
+                "\n".format(equals=delimiter,
+                            default_section=self.default_section)
                 )
-        self.assertEqual(output.getvalue(), expect_string)
+            if self.allow_no_value:
+                expect_string += (
+                    "[Valueless]\n"
+                    "option-without-value\n"
+                    "\n"
+                    )
+            self.assertEqual(output.getvalue(), expect_string)
 
     def test_set_string_types(self):
         cf = self.fromstring("[sect]\n"
@@ -687,15 +752,17 @@
             "name{equals}%(reference)s\n".format(equals=self.delimiters[0]))
 
     def check_items_config(self, expected):
-        cf = self.fromstring(
-            "[section]\n"
-            "name {0[0]} value\n"
-            "key{0[1]} |%(name)s| \n"
-            "getdefault{0[1]} |%(default)s|\n".format(self.delimiters),
-            defaults={"default": "<default>"})
-        L = list(cf.items("section"))
+        cf = self.fromstring("""
+            [section]
+            name {0[0]} %(value)s
+            key{0[1]} |%(name)s|
+            getdefault{0[1]} |%(default)s|
+        """.format(self.delimiters), defaults={"default": "<default>"})
+        L = list(cf.items("section", vars={'value': 'value'}))
         L.sort()
         self.assertEqual(L, expected)
+        with self.assertRaises(configparser.NoSectionError):
+            cf.items("no such section")
 
 
 class StrictTestCase(BasicTestCase):
@@ -739,7 +806,8 @@
         self.check_items_config([('default', '<default>'),
                                  ('getdefault', '|<default>|'),
                                  ('key', '|value|'),
-                                 ('name', 'value')])
+                                 ('name', 'value'),
+                                 ('value', 'value')])
 
     def test_safe_interpolation(self):
         # See http://www.python.org/sf/511737
@@ -866,7 +934,8 @@
         self.check_items_config([('default', '<default>'),
                                  ('getdefault', '|%(default)s|'),
                                  ('key', '|%(name)s|'),
-                                 ('name', 'value')])
+                                 ('name', '%(value)s'),
+                                 ('value', 'value')])
 
     def test_set_nonstring_types(self):
         cf = self.newconfig()
@@ -970,11 +1039,60 @@
 
             [one for me]
             pong = ${one for you:ping}
+
+            [selfish]
+            me = ${me}
         """).strip())
 
         with self.assertRaises(configparser.InterpolationDepthError):
             cf['one for you']['ping']
+        with self.assertRaises(configparser.InterpolationDepthError):
+            cf['selfish']['me']
+
+    def test_strange_options(self):
+        cf = self.fromstring("""
+            [dollars]
+            $var = $$value
+            $var2 = ${$var}
+            ${sick} = cannot interpolate me
+
+            [interpolated]
+            $other = ${dollars:$var}
+            $trying = ${dollars:${sick}}
+        """)
+
+        self.assertEqual(cf['dollars']['$var'], '$value')
+        self.assertEqual(cf['interpolated']['$other'], '$value')
+        self.assertEqual(cf['dollars']['${sick}'], 'cannot interpolate me')
+        exception_class = configparser.InterpolationMissingOptionError
+        with self.assertRaises(exception_class) as cm:
+            cf['interpolated']['$trying']
+        self.assertEqual(cm.exception.reference, 'dollars:${sick')
+        self.assertEqual(cm.exception.args[2], '}') #rawval
+
+
+    def test_other_errors(self):
+        cf = self.fromstring("""
+            [interpolation fail]
+            case1 = ${where's the brace
+            case2 = ${does_not_exist}
+            case3 = ${wrong_section:wrong_value}
+            case4 = ${i:like:colon:characters}
+            case5 = $100 for Fail No 5!
+        """)
 
+        with self.assertRaises(configparser.InterpolationSyntaxError):
+            cf['interpolation fail']['case1']
+        with self.assertRaises(configparser.InterpolationMissingOptionError):
+            cf['interpolation fail']['case2']
+        with self.assertRaises(configparser.InterpolationMissingOptionError):
+            cf['interpolation fail']['case3']
+        with self.assertRaises(configparser.InterpolationSyntaxError):
+            cf['interpolation fail']['case4']
+        with self.assertRaises(configparser.InterpolationSyntaxError):
+            cf['interpolation fail']['case5']
+        with self.assertRaises(ValueError):
+            cf['interpolation fail']['case6'] = "BLACK $ABBATH"
 
 
 class ConfigParserTestCaseNoValue(ConfigParserTestCase):
@@ -1093,10 +1211,114 @@
         ; a space must precede an inline comment
         """)
         cf = self.fromstring(config_string)
-        self.assertEqual(cf.get('Commented Bar', 'foo'), 'bar # not a comment!')
+        self.assertEqual(cf.get('Commented Bar', 'foo'),
+                         'bar # not a comment!')
         self.assertEqual(cf.get('Commented Bar', 'baz'), 'qwe')
-        self.assertEqual(cf.get('Commented Bar', 'quirk'), 'this;is not a comment')
+        self.assertEqual(cf.get('Commented Bar', 'quirk'),
+                         'this;is not a comment')
 
+class CopyTestCase(BasicTestCase):
+    config_class = configparser.ConfigParser
+
+    def fromstring(self, string, defaults=None):
+        cf = self.newconfig(defaults)
+        cf.read_string(string)
+        cf_copy = self.newconfig()
+        cf_copy.read_dict(cf)
+        # we have to clean up option duplicates that appeared because of
+        # the magic DEFAULTSECT behaviour.
+        for section in cf_copy.values():
+            if section.name == self.default_section:
+                continue
+            for default, value in cf[self.default_section].items():
+                if section[default] == value:
+                    del section[default]
+        return cf_copy
+
+class CoverageOneHundredTestCase(unittest.TestCase):
+    """Covers edge cases in the codebase."""
+
+    def test_duplicate_option_error(self):
+        error = configparser.DuplicateOptionError('section', 'option')
+        self.assertEqual(error.section, 'section')
+        self.assertEqual(error.option, 'option')
+        self.assertEqual(error.source, None)
+        self.assertEqual(error.lineno, None)
+        self.assertEqual(error.args, ('section', 'option', None, None))
+        self.assertEqual(str(error), "Option 'option' in section 'section' "
+                                     "already exists")
+
+    def test_interpolation_depth_error(self):
+        error = configparser.InterpolationDepthError('option', 'section',
+                                                     'rawval')
+        self.assertEqual(error.args, ('option', 'section', 'rawval'))
+        self.assertEqual(error.option, 'option')
+        self.assertEqual(error.section, 'section')
+
+    def test_parsing_error(self):
+        with self.assertRaises(ValueError) as cm:
+            configparser.ParsingError()
+        self.assertEqual(str(cm.exception), "Required argument `source' not "
+                                            "given.")
+        with self.assertRaises(ValueError) as cm:
+            configparser.ParsingError(source='source', filename='filename')
+        self.assertEqual(str(cm.exception), "Cannot specify both `filename' "
+                                            "and `source'. Use `source'.")
+        error = configparser.ParsingError(filename='source')
+        self.assertEqual(error.source, 'source')
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always", DeprecationWarning)
+            self.assertEqual(error.filename, 'source')
+            error.filename = 'filename'
+            self.assertEqual(error.source, 'filename')
+        for warning in w:
+            self.assertTrue(warning.category is DeprecationWarning)
+
+    def test_interpolation_validation(self):
+        parser = configparser.ConfigParser()
+        parser.read_string("""
+            [section]
+            invalid_percent = %
+            invalid_reference = %(()
+            invalid_variable = %(does_not_exist)s
+        """)
+        with self.assertRaises(configparser.InterpolationSyntaxError) as cm:
+            parser['section']['invalid_percent']
+        self.assertEqual(str(cm.exception), "'%' must be followed by '%' or "
+                                            "'(', found: '%'")
+        with self.assertRaises(configparser.InterpolationSyntaxError) as cm:
+            parser['section']['invalid_reference']
+        self.assertEqual(str(cm.exception), "bad interpolation variable "
+                                            "reference '%(()'")
+
+    def test_readfp_deprecation(self):
+        sio = io.StringIO("""
+        [section]
+        option = value
+        """)
+        parser = configparser.ConfigParser()
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always", DeprecationWarning)
+            parser.readfp(sio, filename='StringIO')
+        for warning in w:
+            self.assertTrue(warning.category is DeprecationWarning)
+        self.assertEqual(len(parser), 2)
+        self.assertEqual(parser['section']['option'], 'value')
+
+    def test_safeconfigparser_deprecation(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always", DeprecationWarning)
+            parser = configparser.SafeConfigParser()
+        for warning in w:
+            self.assertTrue(warning.category is DeprecationWarning)
+
+    def test_sectionproxy_repr(self):
+        parser = configparser.ConfigParser()
+        parser.read_string("""
+            [section]
+            key = value
+        """)
+        self.assertEqual(repr(parser['section']), '<Section: section>')
 
 def test_main():
     support.run_unittest(
@@ -1114,20 +1336,7 @@
         Issue7005TestCase,
         StrictTestCase,
         CompatibleTestCase,
+        CopyTestCase,
         ConfigParserTestCaseNonStandardDefaultSection,
+        CoverageOneHundredTestCase,
         )
-
-def test_coverage(coverdir):
-    trace = support.import_module('trace')
-    tracer=trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix,], trace=0,
-                       count=1)
-    tracer.run('test_main()')
-    r=tracer.results()
-    print("Writing coverage results...")
-    r.write_results(show_missing=True, summary=True, coverdir=coverdir)
-
-if __name__ == "__main__":
-    if "-c" in sys.argv:
-        test_coverage('/tmp/configparser.cover')
-    else:
-        test_main()

Modified: python/branches/py3k/Misc/NEWS
==============================================================================
--- python/branches/py3k/Misc/NEWS	(original)
+++ python/branches/py3k/Misc/NEWS	Fri Dec 17 22:56:32 2010
@@ -293,6 +293,8 @@
 - Issue #10467: Fix BytesIO.readinto() after seeking into a position after the
   end of the file.
 
+- configparser: 100% test coverage.
+
 - Issue #10499: configparser supports pluggable interpolation handlers. The
   default classic interpolation handler is called BasicInterpolation. Another
   interpolation handler added (ExtendedInterpolation) which supports the syntax
@@ -314,7 +316,9 @@
 - Issue #9421: configparser's getint(), getfloat() and getboolean() methods
   accept vars and default arguments just like get() does.
 
-- Issue #9452: configparser supports reading from strings and dictionaries.
+- Issue #9452: configparser supports reading from strings and dictionaries
+  (thanks to the mapping protocol API, the latter can be used to copy data
+  between parsers).
 
 - configparser: accepted INI file structure is now customizable, including
   comment prefixes, name of the DEFAULT section, empty lines in multiline


More information about the Python-checkins mailing list