Index: Lib/configparser.py =================================================================== --- Lib/configparser.py (revision 83147) +++ Lib/configparser.py (working copy) @@ -142,6 +142,7 @@ def __init__(self, section): Error.__init__(self, 'No section: %r' % (section,)) self.section = section + self.args = (section, ) class DuplicateSectionError(Error): """Raised when a section is multiply-created.""" @@ -149,6 +150,7 @@ def __init__(self, section): Error.__init__(self, "Section %r already exists" % section) self.section = section + self.args = (section, ) class NoOptionError(Error): """A requested option was not found.""" @@ -158,6 +160,7 @@ (option, section)) self.option = option self.section = section + self.args = (option, section) class InterpolationError(Error): """Base class for interpolation-related exceptions.""" @@ -166,6 +169,7 @@ Error.__init__(self, msg) self.option = option self.section = section + self.args = (option, section, msg) class InterpolationMissingOptionError(InterpolationError): """A string substitution required a setting which was not available.""" @@ -179,6 +183,7 @@ % (section, option, reference, rawval)) InterpolationError.__init__(self, option, section, msg) self.reference = reference + self.args = (option, section, rawval, reference) class InterpolationSyntaxError(InterpolationError): """Raised when the source text into which substitutions are made @@ -194,6 +199,7 @@ "\trawval : %s\n" % (section, option, rawval)) InterpolationError.__init__(self, option, section, msg) + self.args = (option, section, rawval) class ParsingError(Error): """Raised when a configuration file does not follow legal syntax.""" @@ -202,6 +208,7 @@ Error.__init__(self, 'File contains parsing errors: %s' % filename) self.filename = filename self.errors = [] + self.args = (filename, ) def append(self, lineno, line): self.errors.append((lineno, line)) @@ -218,8 +225,8 @@ self.filename = filename self.lineno = lineno self.line = line + self.args = (filename, lineno, line) - class RawConfigParser: def __init__(self, defaults=None, dict_type=_default_dict, allow_no_value=False): @@ -398,12 +405,11 @@ for section in self._sections: fp.write("[%s]\n" % section) for (key, value) in self._sections[section].items(): - if key != "__name__": - if value is None: - fp.write("%s\n" % (key)) - else: - fp.write("%s = %s\n" % - (key, str(value).replace('\n', '\n\t'))) + if key == "__name__": + continue + if value is not None: + key = " = ".join((key, str(value).replace('\n', '\n\t'))) + fp.write("%s\n" % (key)) fp.write("\n") def remove_option(self, section, option): @@ -464,10 +470,10 @@ leading whitespace. Blank lines, lines beginning with a '#', and just about everything else are ignored. """ - cursect = None # None, or a dictionary + cursect = None # None, or a dictionary optname = None lineno = 0 - e = None # None, or an exception + e = None # None, or an exception while True: line = fp.readline() if not line: @@ -483,7 +489,7 @@ if line[0].isspace() and cursect is not None and optname: value = line.strip() if value: - cursect[optname] = "%s\n%s" % (cursect[optname], value) + cursect[optname].append(value) # a section header or option header? else: # is it a section header? @@ -508,6 +514,7 @@ mo = self._optcre.match(line) if mo: optname, vi, optval = mo.group('option', 'vi', 'value') + optname = self.optionxform(optname.rstrip()) # This check is fine because the OPTCRE cannot # match if it would set optval to None if optval is not None: @@ -518,11 +525,13 @@ if pos != -1 and optval[pos-1].isspace(): optval = optval[:pos] optval = optval.strip() - # allow empty values - if optval == '""': - optval = '' - optname = self.optionxform(optname.rstrip()) - cursect[optname] = optval + # allow empty values + if optval == '""': + optval = '' + cursect[optname] = [optval] + else: + # valueless option handling + cursect[optname] = optval else: # a non-fatal parsing error occurred. set up the # exception but keep going. the exception will be @@ -535,7 +544,15 @@ if e: raise e + # join the multi-line values collected while reading + all_sections = [self._defaults] + all_sections.extend(self._sections.values()) + for options in all_sections: + for name, val in options.items(): + if isinstance(val, list): + options[name] = '\n'.join(val) + class ConfigParser(RawConfigParser): def get(self, section, option, raw=False, vars=None): Index: Lib/test/test_cfgparser.py =================================================================== --- Lib/test/test_cfgparser.py (revision 83147) +++ Lib/test/test_cfgparser.py (working copy) @@ -1,7 +1,8 @@ +import collections import configparser import io +import os import unittest -import collections from test import support @@ -107,9 +108,10 @@ "remove_option() failed to report non-existence of option" " that was removed") - self.assertRaises(configparser.NoSectionError, - cf.remove_option, 'No Such Section', 'foo') - + with self.assertRaises(configparser.NoSectionError) as cm: + cf.remove_option('No Such Section', 'foo') + self.assertEqual(cm.exception.args, ('No Such Section',)) + eq(cf.get('Long Line', 'foo'), 'this line is much, much longer than my editor\nlikes it.') @@ -160,20 +162,24 @@ def test_parse_errors(self): self.newconfig() - self.parse_error(configparser.ParsingError, + e = self.parse_error(configparser.ParsingError, "[Foo]\n extra-spaces: splat\n") + self.assertEqual(e.args, ('',)) self.parse_error(configparser.ParsingError, "[Foo]\n extra-spaces= splat\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") - self.parse_error(configparser.MissingSectionHeaderError, + e = self.parse_error(configparser.MissingSectionHeaderError, "No Section!\n") + self.assertEqual(e.args, ('', 1, "No Section!\n")) def parse_error(self, exc, src): sio = io.StringIO(src) - self.assertRaises(exc, self.cf.readfp, sio) + with self.assertRaises(exc) as cm: + self.cf.readfp(sio) + return cm.exception def test_query_errors(self): cf = self.newconfig() @@ -181,13 +187,15 @@ "new ConfigParser should have no defined sections") self.assertFalse(cf.has_section("Foo"), "new ConfigParser should have no acknowledged sections") - self.assertRaises(configparser.NoSectionError, - cf.options, "Foo") - self.assertRaises(configparser.NoSectionError, - cf.set, "foo", "bar", "value") - self.get_error(configparser.NoSectionError, "foo", "bar") + with self.assertRaises(configparser.NoSectionError) as cm: + cf.options("Foo") + with self.assertRaises(configparser.NoSectionError) as cm: + cf.set("foo", "bar", "value") + e = self.get_error(configparser.NoSectionError, "foo", "bar") + self.assertEqual(e.args, ("foo",)) cf.add_section("foo") - self.get_error(configparser.NoOptionError, "foo", "bar") + e = self.get_error(configparser.NoOptionError, "foo", "bar") + self.assertEqual(e.args, ("bar", "foo")) def get_error(self, exc, section, option): try: @@ -226,8 +234,9 @@ def test_weird_errors(self): cf = self.newconfig() cf.add_section("Foo") - self.assertRaises(configparser.DuplicateSectionError, - cf.add_section, "Foo") + with self.assertRaises(configparser.DuplicateSectionError) as cm: + cf.add_section("Foo") + self.assertEqual(cm.exception.args, ("Foo",)) def test_write(self): config_string = ( @@ -346,6 +355,11 @@ config_class = configparser.ConfigParser def test_interpolation(self): + rawval = { + configparser.ConfigParser: "something %(with11)s "\ + "lots of interpolation (11 steps)", + configparser.SafeConfigParser: "%(with1)s", + } cf = self.get_interpolation_config() eq = self.assertEqual eq(cf.get("Foo", "getname"), "Foo") @@ -354,15 +368,22 @@ "something with lots of interpolation (9 steps)") eq(cf.get("Foo", "bar10"), "something with lots of interpolation (10 steps)") - self.get_error(configparser.InterpolationDepthError, "Foo", "bar11") + e = self.get_error(configparser.InterpolationDepthError, "Foo", "bar11") + self.assertEqual(e.args, ("bar11", "Foo", rawval[self.config_class])) def test_interpolation_missing_value(self): + rawval = { + configparser.ConfigParser: '%(reference)s', + configparser.SafeConfigParser: '', + } self.get_interpolation_config() - e = self.get_error(configparser.InterpolationError, + e = self.get_error(configparser.InterpolationMissingOptionError, "Interpolation Error", "name") self.assertEqual(e.reference, "reference") self.assertEqual(e.section, "Interpolation Error") self.assertEqual(e.option, "name") + self.assertEqual(e.args, ('name', 'Interpolation Error', + rawval[self.config_class], 'reference')) def test_items(self): self.check_items_config([('default', ''), @@ -392,7 +413,34 @@ self.assertRaises(ValueError, cf.get, 'non-string', 'string_with_interpolation', raw=False) +class MultilineValuesTestCase(TestCaseBase): + config_class = configparser.ConfigParser + wonderful_spam = "I'm having spam spam spam spam "\ + "spam spam spam beaked beans spam "\ + "spam spam and spam!".replace(' ', '\t\n') + def setUp(self): + cf = self.newconfig() + for i in range(100): + s = 'section{}'.format(i) + cf.add_section(s) + for j in range(10): + cf.set(s, 'lovely_spam{}'.format(j), self.wonderful_spam) + with open(support.TESTFN, 'w') as f: + cf.write(f) + + def tearDown(self): + os.unlink(support.TESTFN) + + def test_dominating_multiline_values(self): + # we're reading from file because this is where the code changed + # during performance updates in Python 3.2 + cf_from_file = self.newconfig() + with open(support.TESTFN) as f: + cf_from_file.readfp(f) + self.assertEqual(cf_from_file.get('section8', 'lovely_spam4'), + self.wonderful_spam.replace('\t\n', '\n')) + class RawConfigParserTestCase(TestCaseBase): config_class = configparser.RawConfigParser @@ -510,10 +558,11 @@ def test_main(): support.run_unittest( ConfigParserTestCase, + MultilineValuesTestCase, RawConfigParserTestCase, SafeConfigParserTestCase, + SafeConfigParserTestCaseNoValue, SortedTestCase, - SafeConfigParserTestCaseNoValue, )