diff options
-rw-r--r-- | Doc/library/configparser.rst | 59 | ||||
-rw-r--r-- | Lib/configparser.py | 62 | ||||
-rw-r--r-- | Lib/test/test_cfgparser.py | 77 |
3 files changed, 156 insertions, 42 deletions
diff --git a/Doc/library/configparser.rst b/Doc/library/configparser.rst index 1d097f9..34a40ee 100644 --- a/Doc/library/configparser.rst +++ b/Doc/library/configparser.rst @@ -56,19 +56,28 @@ dictionary type is passed that sorts its keys, the sections will be sorted on write-back, as will be the keys within each section. -.. class:: RawConfigParser(defaults=None, dict_type=collections.OrderedDict) +.. class:: RawConfigParser(defaults=None, dict_type=collections.OrderedDict, + allow_no_value=False) 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. .. versionchanged:: 3.1 The default *dict_type* is :class:`collections.OrderedDict`. + .. versionchanged:: 3.2 + *allow_no_value* was added. + -.. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict) +.. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict, + allow_no_value=False) Derived class of :class:`RawConfigParser` that implements the magical interpolation feature and adds optional arguments to the :meth:`get` and @@ -86,8 +95,12 @@ write-back, as will be the keys within each section. .. versionchanged:: 3.1 The default *dict_type* is :class:`collections.OrderedDict`. + .. versionchanged:: 3.2 + *allow_no_value* was added. + -.. class:: SafeConfigParser(defaults=None, dict_type=collections.OrderedDict) +.. class:: SafeConfigParser(defaults=None, dict_type=collections.OrderedDict, + allow_no_value=False) Derived class of :class:`ConfigParser` that implements a more-sane variant of the magical interpolation feature. This implementation is more predictable as @@ -99,6 +112,9 @@ write-back, as will be the keys within each section. .. versionchanged:: 3.1 The default *dict_type* is :class:`collections.OrderedDict`. + .. versionchanged:: 3.2 + *allow_no_value* was added. + .. exception:: NoSectionError @@ -447,3 +463,38 @@ The function ``opt_move`` below can be used to move options between sections:: 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' diff --git a/Lib/configparser.py b/Lib/configparser.py index bfbe4cb..d0b03f9 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -221,10 +221,15 @@ class MissingSectionHeaderError(ParsingError): 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 @@ class RawConfigParser: 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 @@ class RawConfigParser: 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 @@ class RawConfigParser: # 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 @@ class RawConfigParser: 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 @@ class ConfigParser(RawConfigParser): 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 @@ class ConfigParser(RawConfigParser): 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 @@ class ConfigParser(RawConfigParser): 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 @@ class SafeConfigParser(ConfigParser): 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, str): - 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, str): + raise TypeError("option values must be strings") # check for bad percent signs: # first, replace all "good" interpolations tmp_value = value.replace('%%', '') diff --git a/Lib/test/test_cfgparser.py b/Lib/test/test_cfgparser.py index 6271d6f..6acab6b 100644 --- a/Lib/test/test_cfgparser.py +++ b/Lib/test/test_cfgparser.py @@ -6,6 +6,7 @@ import collections from test import support class SortedDict(collections.UserDict): + def items(self): return sorted(self.data.items()) @@ -20,12 +21,16 @@ class SortedDict(collections.UserDict): __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): @@ -35,7 +40,7 @@ class TestCaseBase(unittest.TestCase): return cf def test_basic(self): - cf = self.fromstring( + config_string = ( "[Foo Bar]\n" "foo=bar\n" "[Spacey Bar]\n" @@ -55,17 +60,28 @@ class TestCaseBase(unittest.TestCase): "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: @@ -75,6 +91,8 @@ class TestCaseBase(unittest.TestCase): 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!') @@ -147,8 +165,6 @@ class TestCaseBase(unittest.TestCase): 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") @@ -214,18 +230,24 @@ class TestCaseBase(unittest.TestCase): 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 = io.StringIO() cf.write(output) - self.assertEqual( - output.getvalue(), + expect_string = ( "[DEFAULT]\n" "foo = another very\n" "\tlong line\n" @@ -235,6 +257,13 @@ class TestCaseBase(unittest.TestCase): "\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" @@ -328,7 +357,7 @@ class ConfigParserTestCase(TestCaseBase): 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") @@ -448,6 +477,11 @@ class SafeConfigParserTestCase(ConfigParserTestCase): 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) @@ -472,13 +506,16 @@ class SortedTestCase(RawConfigParserTestCase): "o3 = 2\n" "o4 = 1\n\n") + def test_main(): support.run_unittest( ConfigParserTestCase, RawConfigParserTestCase, SafeConfigParserTestCase, - SortedTestCase - ) + SortedTestCase, + SafeConfigParserTestCaseNoValue, + ) + if __name__ == "__main__": test_main() |