[Python-checkins] r78232 - in python/trunk: Doc/library/configparser.rst Lib/ConfigParser.py Lib/test/test_cfgparser.py

fred.drake python-checkins at python.org
Fri Feb 19 06:24:31 CET 2010


Author: fred.drake
Date: Fri Feb 19 06:24:30 2010
New Revision: 78232

Log:
- apply patch from issue 7005
- add corresponding documentation


Modified:
   python/trunk/Doc/library/configparser.rst
   python/trunk/Lib/ConfigParser.py
   python/trunk/Lib/test/test_cfgparser.py

Modified: python/trunk/Doc/library/configparser.rst
==============================================================================
--- python/trunk/Doc/library/configparser.rst	(original)
+++ python/trunk/Doc/library/configparser.rst	Fri Feb 19 06:24:30 2010
@@ -62,12 +62,16 @@
 write-back, as will be the keys within each section.
 
 
-.. class:: RawConfigParser([defaults[, dict_type]])
+.. class:: RawConfigParser([defaults[, dict_type[, allow_no_value]]])
 
    The basic configuration object.  When *defaults* is given, it is initialized
    into the dictionary of intrinsic defaults.  When *dict_type* is given, it will
    be used to create the dictionary objects for the list of sections, for the
-   options within a section, and for the default values. This class does not
+   options within a section, and for the default values.  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.
 
    .. versionadded:: 2.3
@@ -77,9 +81,10 @@
 
    .. versionchanged:: 2.7
       The default *dict_type* is :class:`collections.OrderedDict`.
+      *allow_no_value* was added.
 
 
-.. class:: ConfigParser([defaults[, dict_type]])
+.. class:: ConfigParser([defaults[, dict_type[, allow_no_value]]])
 
    Derived class of :class:`RawConfigParser` that implements the magical
    interpolation feature and adds optional arguments to the :meth:`get` and
@@ -101,9 +106,10 @@
 
    .. versionchanged:: 2.7
       The default *dict_type* is :class:`collections.OrderedDict`.
+      *allow_no_value* was added.
 
 
-.. class:: SafeConfigParser([defaults[, dict_type]])
+.. class:: SafeConfigParser([defaults[, dict_type[, allow_no_value]]])
 
    Derived class of :class:`ConfigParser` that implements a more-sane variant of
    the magical interpolation feature.  This implementation is more predictable as
@@ -119,6 +125,7 @@
 
    .. versionchanged:: 2.7
       The default *dict_type* is :class:`collections.OrderedDict`.
+      *allow_no_value* was added.
 
 
 .. exception:: NoSectionError
@@ -484,3 +491,38 @@
            opt_move(config, section1, section2, option)
        else:
            config.remove_option(section1, option)
+
+Some configuration files are known to include settings without values, but which
+otherwise conform to the syntax supported by :mod:`ConfigParser`.  The
+*allow_no_value* parameter to the constructor can be used to indicate that such
+values should be accepted:
+
+.. doctest::
+
+   >>> import ConfigParser
+   >>> import io
+
+   >>> sample_config = """
+   ... [mysqld]
+   ... user = mysql
+   ... pid-file = /var/run/mysqld/mysqld.pid
+   ... skip-external-locking
+   ... old_passwords = 1
+   ... skip-bdb
+   ... skip-innodb
+   ... """
+   >>> config = ConfigParser.RawConfigParser(allow_no_value=True)
+   >>> config.readfp(io.BytesIO(sample_config))
+
+   >>> # Settings with values are treated as before:
+   >>> config.get("mysqld", "user")
+   'mysql'
+
+   >>> # Settings without values provide None:
+   >>> config.get("mysqld", "skip-bdb")
+
+   >>> # Settings which aren't specified still raise an error:
+   >>> config.get("mysqld", "does-not-exist")
+   Traceback (most recent call last):
+     ...
+   ConfigParser.NoOptionError: No option 'does-not-exist' in section: 'mysqld'

Modified: python/trunk/Lib/ConfigParser.py
==============================================================================
--- python/trunk/Lib/ConfigParser.py	(original)
+++ python/trunk/Lib/ConfigParser.py	Fri Feb 19 06:24:30 2010
@@ -221,10 +221,15 @@
 
 
 class RawConfigParser:
-    def __init__(self, defaults=None, dict_type=_default_dict):
+    def __init__(self, defaults=None, dict_type=_default_dict,
+                 allow_no_value=False):
         self._dict = dict_type
         self._sections = self._dict()
         self._defaults = self._dict()
+        if allow_no_value:
+            self._optcre = self.OPTCRE_NV
+        else:
+            self._optcre = self.OPTCRE
         if defaults:
             for key, value in defaults.items():
                 self._defaults[self.optionxform(key)] = value
@@ -372,7 +377,7 @@
             return (option in self._sections[section]
                     or option in self._defaults)
 
-    def set(self, section, option, value):
+    def set(self, section, option, value=None):
         """Set an option."""
         if not section or section == DEFAULTSECT:
             sectdict = self._defaults
@@ -394,8 +399,11 @@
             fp.write("[%s]\n" % section)
             for (key, value) in self._sections[section].items():
                 if key != "__name__":
-                    fp.write("%s = %s\n" %
-                             (key, str(value).replace('\n', '\n\t')))
+                    if value is None:
+                        fp.write("%s\n" % (key))
+                    else:
+                        fp.write("%s = %s\n" %
+                                 (key, str(value).replace('\n', '\n\t')))
             fp.write("\n")
 
     def remove_option(self, section, option):
@@ -436,6 +444,15 @@
                                               # by any # space/tab
         r'(?P<value>.*)$'                     # everything up to eol
         )
+    OPTCRE_NV = re.compile(
+        r'(?P<option>[^:=\s][^:=]*)'          # very permissive!
+        r'\s*(?:'                             # any number of space/tab,
+        r'(?P<vi>[:=])\s*'                    # optionally followed by
+                                              # separator (either : or
+                                              # =), followed by any #
+                                              # space/tab
+        r'(?P<value>.*))?$'                   # everything up to eol
+        )
 
     def _read(self, fp, fpname):
         """Parse a sectioned setup file.
@@ -488,16 +505,19 @@
                     raise MissingSectionHeaderError(fpname, lineno, line)
                 # an option line?
                 else:
-                    mo = self.OPTCRE.match(line)
+                    mo = self._optcre.match(line)
                     if mo:
                         optname, vi, optval = mo.group('option', 'vi', 'value')
-                        if vi in ('=', ':') and ';' in optval:
-                            # ';' is a comment delimiter only if it follows
-                            # a spacing character
-                            pos = optval.find(';')
-                            if pos != -1 and optval[pos-1].isspace():
-                                optval = optval[:pos]
-                        optval = optval.strip()
+                        # This check is fine because the OPTCRE cannot
+                        # match if it would set optval to None
+                        if optval is not None:
+                            if vi in ('=', ':') and ';' in optval:
+                                # ';' is a comment delimiter only if it follows
+                                # a spacing character
+                                pos = optval.find(';')
+                                if pos != -1 and optval[pos-1].isspace():
+                                    optval = optval[:pos]
+                            optval = optval.strip()
                         # allow empty values
                         if optval == '""':
                             optval = ''
@@ -545,7 +565,7 @@
         except KeyError:
             raise NoOptionError(option, section)
 
-        if raw:
+        if raw or value is None:
             return value
         else:
             return self._interpolate(section, option, value, d)
@@ -588,7 +608,7 @@
         depth = MAX_INTERPOLATION_DEPTH
         while depth:                    # Loop through this until it's done
             depth -= 1
-            if "%(" in value:
+            if value and "%(" in value:
                 value = self._KEYCRE.sub(self._interpolation_replace, value)
                 try:
                     value = value % vars
@@ -597,7 +617,7 @@
                         option, section, rawval, e.args[0])
             else:
                 break
-        if "%(" in value:
+        if value and "%(" in value:
             raise InterpolationDepthError(option, section, rawval)
         return value
 
@@ -659,10 +679,16 @@
                     option, section,
                     "'%%' must be followed by '%%' or '(', found: %r" % (rest,))
 
-    def set(self, section, option, value):
+    def set(self, section, option, value=None):
         """Set an option.  Extend ConfigParser.set: check for string values."""
-        if not isinstance(value, basestring):
-            raise TypeError("option values must be strings")
+        # The only legal non-string value if we allow valueless
+        # options is None, so we need to check if the value is a
+        # string if:
+        # - we do not allow valueless options, or
+        # - we allow valueless options but the value is not None
+        if self._optcre is self.OPTCRE or value:
+            if not isinstance(value, basestring):
+                raise TypeError("option values must be strings")
         # check for bad percent signs:
         # first, replace all "good" interpolations
         tmp_value = value.replace('%%', '')

Modified: python/trunk/Lib/test/test_cfgparser.py
==============================================================================
--- python/trunk/Lib/test/test_cfgparser.py	(original)
+++ python/trunk/Lib/test/test_cfgparser.py	Fri Feb 19 06:24:30 2010
@@ -5,6 +5,7 @@
 
 from test import test_support
 
+
 class SortedDict(UserDict.UserDict):
     def items(self):
         result = self.data.items()
@@ -26,12 +27,16 @@
     __iter__ = iterkeys
     def itervalues(self): return iter(self.values())
 
+
 class TestCaseBase(unittest.TestCase):
+    allow_no_value = False
+
     def newconfig(self, defaults=None):
         if defaults is None:
-            self.cf = self.config_class()
+            self.cf = self.config_class(allow_no_value=self.allow_no_value)
         else:
-            self.cf = self.config_class(defaults)
+            self.cf = self.config_class(defaults,
+                                        allow_no_value=self.allow_no_value)
         return self.cf
 
     def fromstring(self, string, defaults=None):
@@ -41,7 +46,7 @@
         return cf
 
     def test_basic(self):
-        cf = self.fromstring(
+        config_string = (
             "[Foo Bar]\n"
             "foo=bar\n"
             "[Spacey Bar]\n"
@@ -61,17 +66,28 @@
             "key with spaces : value\n"
             "another with spaces = splat!\n"
             )
+        if self.allow_no_value:
+            config_string += (
+                "[NoValue]\n"
+                "option-without-value\n"
+                )
+
+        cf = self.fromstring(config_string)
         L = cf.sections()
         L.sort()
+        E = [r'Commented Bar',
+             r'Foo Bar',
+             r'Internationalized Stuff',
+             r'Long Line',
+             r'Section\with$weird%characters[' '\t',
+             r'Spaces',
+             r'Spacey Bar',
+             ]
+        if self.allow_no_value:
+            E.append(r'NoValue')
+        E.sort()
         eq = self.assertEqual
-        eq(L, [r'Commented Bar',
-               r'Foo Bar',
-               r'Internationalized Stuff',
-               r'Long Line',
-               r'Section\with$weird%characters[' '\t',
-               r'Spaces',
-               r'Spacey Bar',
-               ])
+        eq(L, E)
 
         # The use of spaces in the section names serves as a
         # regression test for SourceForge bug #583248:
@@ -81,6 +97,8 @@
         eq(cf.get('Commented Bar', 'foo'), 'bar')
         eq(cf.get('Spaces', 'key with spaces'), 'value')
         eq(cf.get('Spaces', 'another with spaces'), 'splat!')
+        if self.allow_no_value:
+            eq(cf.get('NoValue', 'option-without-value'), None)
 
         self.assertNotIn('__name__', cf.options("Foo Bar"),
                          '__name__ "option" should not be exposed by the API!')
@@ -153,8 +171,6 @@
         self.parse_error(ConfigParser.ParsingError,
                          "[Foo]\n  extra-spaces= splat\n")
         self.parse_error(ConfigParser.ParsingError,
-                         "[Foo]\noption-without-value\n")
-        self.parse_error(ConfigParser.ParsingError,
                          "[Foo]\n:value-without-option-name\n")
         self.parse_error(ConfigParser.ParsingError,
                          "[Foo]\n=value-without-option-name\n")
@@ -220,18 +236,24 @@
                           cf.add_section, "Foo")
 
     def test_write(self):
-        cf = self.fromstring(
+        config_string = (
             "[Long Line]\n"
             "foo: this line is much, much longer than my editor\n"
             "   likes it.\n"
             "[DEFAULT]\n"
             "foo: another very\n"
-            " long line"
+            " long line\n"
+            )
+        if self.allow_no_value:
+            config_string += (
+            "[Valueless]\n"
+            "option-without-value\n"
             )
+
+        cf = self.fromstring(config_string)
         output = StringIO.StringIO()
         cf.write(output)
-        self.assertEqual(
-            output.getvalue(),
+        expect_string = (
             "[DEFAULT]\n"
             "foo = another very\n"
             "\tlong line\n"
@@ -241,6 +263,13 @@
             "\tlikes it.\n"
             "\n"
             )
+        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"
@@ -339,7 +368,7 @@
         self.get_error(ConfigParser.InterpolationDepthError, "Foo", "bar11")
 
     def test_interpolation_missing_value(self):
-        cf = self.get_interpolation_config()
+        self.get_interpolation_config()
         e = self.get_error(ConfigParser.InterpolationError,
                            "Interpolation Error", "name")
         self.assertEqual(e.reference, "reference")
@@ -459,6 +488,11 @@
         cf = self.newconfig()
         self.assertRaises(ValueError, cf.add_section, "DEFAULT")
 
+
+class SafeConfigParserTestCaseNoValue(SafeConfigParserTestCase):
+    allow_no_value = True
+
+
 class SortedTestCase(RawConfigParserTestCase):
     def newconfig(self, defaults=None):
         self.cf = self.config_class(defaults=defaults, dict_type=SortedDict)
@@ -483,13 +517,16 @@
                           "o3 = 2\n"
                           "o4 = 1\n\n")
 
+
 def test_main():
     test_support.run_unittest(
         ConfigParserTestCase,
         RawConfigParserTestCase,
         SafeConfigParserTestCase,
-        SortedTestCase
-    )
+        SortedTestCase,
+        SafeConfigParserTestCaseNoValue,
+        )
+
 
 if __name__ == "__main__":
     test_main()


More information about the Python-checkins mailing list