Index: Doc/library/configparser.rst =================================================================== --- Doc/library/configparser.rst (revision 61014) +++ Doc/library/configparser.rst (working copy) @@ -29,9 +29,11 @@ The configuration file consists of sections, led by a ``[section]`` header and followed by ``name: value`` entries, with continuations in the style of -:rfc:`822`; ``name=value`` is also accepted. Note that leading whitespace is -removed from values. The optional values can contain format strings which refer -to other values in the same section, or values in a special ``DEFAULT`` section. +:rfc:`822`; ``name=value`` is also accepted. Note that trailing and leading +whitespaces are removed from values, unless wrapped inside double quotes. +Consequently, double quotes should also be escaped to avoid losing them when +the file is read. The optional values can contain format strings which refer to +other values in the same section, or values in a special ``DEFAULT`` section. Additional defaults can be provided on initialization and retrieval. Lines beginning with ``'#'`` or ``';'`` are ignored and may be used to provide comments. Index: Lib/ConfigParser.py =================================================================== --- Lib/ConfigParser.py (revision 61014) +++ Lib/ConfigParser.py (working copy) @@ -387,6 +387,10 @@ for section in self._sections: fp.write("[%s]\n" % section) for (key, value) in self._sections[section].items(): + # surround with double quotes if leading/trailing spaces or + # double quotes are found + if value[0] in (' ', '"') or value[-1] in (' ', '"'): + value = '"' + value + '"' if key != "__name__": fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t'))) @@ -492,9 +496,11 @@ if pos != -1 and optval[pos-1].isspace(): optval = optval[:pos] optval = optval.strip() - # allow empty values - if optval == '""': - optval = '' + # allow double quotes to be used to prevent trailing + # and leading spaces from being stripped + if len(optval) > 1 and optval[0] == '"' and \ + optval[-1] == '"': + optval = optval[1:-1:] optname = self.optionxform(optname.rstrip()) cursect[optname] = optval else: Index: Lib/test/test_cfgparser.py =================================================================== --- Lib/test/test_cfgparser.py (revision 61014) +++ Lib/test/test_cfgparser.py (working copy) @@ -145,6 +145,22 @@ cf.get("DEFAULT", "Foo"), "Bar", "could not locate option, expecting case-insensitive defaults") + def test_double_quotes(self): + cf = self.fromstring( + "[Spam]\n" + "egg =\"\"\n" + "bacon = \" with trailing and leading spaces \"\n" + "sausage = \"multiple \"double quotes\"\"\n" + "tomatoes = not \"surrounding\"\n" + "more spam = \"\n" + ) + eq = self.assertEqual + eq(cf.get('Spam', 'egg'), '') + eq(cf.get('Spam', 'bacon'), ' with trailing and leading spaces ') + eq(cf.get('Spam', 'sausage'), 'multiple "double quotes"') + eq(cf.get('Spam', 'tomatoes'), 'not "surrounding"') + eq(cf.get('Spam', 'more spam'), '"') + def test_parse_errors(self): self.newconfig() self.parse_error(ConfigParser.ParsingError, @@ -241,6 +257,22 @@ "\n" ) + def test_write_double_quotes(self): + cf = self.fromstring( + "[Escaping]\n" + "bar: \" leading spaces\"\n" + "baz: \"\"double quotes\"\"\n" + ) + output = StringIO.StringIO() + cf.write(output) + self.assertEqual( + output.getvalue(), + "[Escaping]\n" + "bar = \" leading spaces\"\n" + "baz = \"\"double quotes\"\"\n" + "\n" + ) + def test_set_string_types(self): cf = self.fromstring("[sect]\n" "option1=foo\n")