Index: Doc/library/configparser.rst =================================================================== --- Doc/library/configparser.rst (revision 83545) +++ Doc/library/configparser.rst (working copy) @@ -27,8 +27,9 @@ the Windows Registry extended version of INI syntax. A configuration file consists of sections, each led by a ``[section]`` header, -followed by name/value entries separated by a specific string (``=`` or ``:`` by -default). Note that leading whitespace is removed from values. Values can be +followed by key/value entries separated by a specific string (``=`` or ``:`` by +default). By default, section names are case sensitive but keys are not. Leading +and trailing whitespace is removed from keys and from values. Values can be ommitted, in which case the key/value delimiter may also be left out. Values can also span multiple lines, as long as they are indented deeper than the first line of the value. Depending on the parser's mode, blank lines may be treated @@ -194,6 +195,14 @@ that is already present. +.. exception:: DuplicateOptionError + + Exception raised if a single option appears twice during reading from + a single file, string or dictionary. This catches mispellings and + case-sensitivity related errors (a dictionary may have two keys representing + the same case-insensitive configuration key). + + .. exception:: NoOptionError Exception raised when a specified option is not found in the specified section. @@ -325,6 +334,22 @@ for *filename*; the default is ````. +.. method:: RawConfigParser.readstring(string) + + Parse configuration data from a given string. + + .. versionadded:: 3.2 + Introduced in Python 3.2. + + +.. method:: RawConfigParser.readdict(dictionary) + + Parse configuration data from a given dictionary. Each key is a section name + + .. versionadded:: 3.2 + Introduced in Python 3.2. + + .. method:: RawConfigParser.get(section, option) Get an *option* value for the named *section*. Index: Lib/configparser.py =================================================================== --- Lib/configparser.py (revision 83545) +++ Lib/configparser.py (working copy) @@ -114,6 +114,7 @@ # fallback for setup.py which hasn't yet built _collections _default_dict = dict +import io import re import sys @@ -179,6 +180,18 @@ self.args = (section, ) +class DuplicateOptionError(Error): + """Raised when an option is multiply-created from a single file, string + or dictionary.""" + + def __init__(self, section, option): + Error.__init__(self, "Option %r in section %r " + "already exists" % (option, section)) + self.section = section + self.option = option + self.args = (section, option) + + class NoOptionError(Error): """A requested option was not found.""" @@ -409,6 +422,30 @@ filename = '' self._read(fp, filename) + def readstring(self, string): + """Read configuration from a given string.""" + sfile = io.StringIO(string) + self.readfp(sfile, filename='') + + def readdict(self, dictionary): + """Read configuration from a dictionary.""" + options_added = set() + for section, keys in dictionary.items(): + try: + self.add_section(section) + except DuplicateSectionError: + if section in already_ignored: + raise + else: + already_ignored.add(section) + for key, value in keys.items(): + key = self.optionxform(key) + if (section, key) in options_added: + raise DuplicateOptionError(section, key) + else: + options_added.add((section, key)) + self.set(section, key, value) + def get(self, section, option): opt = self.optionxform(option) if section not in self._sections: @@ -552,7 +589,9 @@ an otherwise empty line or may be entered in lines holding values or section names. """ + options_added = set() cursect = None # None, or a dictionary + sectname = None optname = None lineno = 0 indent_level = 0 @@ -618,6 +657,10 @@ if not optname: e = self._handle_error(e, fpname, lineno, line) optname = self.optionxform(optname.rstrip()) + if (sectname, optname) in options_added: + raise DuplicateOptionError(sectname, optname) + else: + options_added.add((sectname, optname)) # This check is fine because the OPTCRE cannot # match if it would set optval to None if optval is not None: @@ -811,13 +854,12 @@ 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('%%', '') - tmp_value = self._interpvar_re.sub('', tmp_value) - # then, check if there's a lone percent sign left - percent_index = tmp_value.find('%') - if percent_index != -1: - raise ValueError("invalid interpolation syntax in %r at " - "position %d" % (value, percent_index)) + # check for bad percent signs + if value: + tmp_value = value.replace('%%', '') # escaped percent signs + tmp_value = self._interpvar_re.sub('', tmp_value) # valid syntax + percent_index = tmp_value.find('%') + if percent_index != -1: + raise ValueError("invalid interpolation syntax in %r at " + "position %d" % (value, percent_index)) ConfigParser.set(self, section, option, value) Index: Lib/test/test_cfgparser.py =================================================================== --- Lib/test/test_cfgparser.py (revision 83545) +++ Lib/test/test_cfgparser.py (working copy) @@ -33,59 +33,23 @@ def newconfig(self, defaults=None): arguments = dict( + defaults=defaults, allow_no_value=self.allow_no_value, delimiters=self.delimiters, comment_prefixes=self.comment_prefixes, empty_lines_in_values=self.empty_lines_in_values, dict_type=self.dict_type, ) - if defaults is None: - self.cf = self.config_class(**arguments) - else: - self.cf = self.config_class(defaults, - **arguments) - return self.cf + return self.config_class(**arguments) def fromstring(self, string, defaults=None): cf = self.newconfig(defaults) - sio = io.StringIO(string) - cf.readfp(sio) + cf.readstring(string) return cf class BasicTestCase(CfgParserTestCaseClass): - def test_basic(self): - config_string = """\ -[Foo Bar] -foo{0[0]}bar -[Spacey Bar] -foo {0[0]} bar -[Spacey Bar From The Beginning] - foo {0[0]} bar - baz {0[0]} qwe -[Commented Bar] -foo{0[1]} bar {1[1]} comment -baz{0[0]}qwe {1[0]}another one -[Long Line] -foo{0[1]} this line is much, much longer than my editor - likes it. -[Section\\with$weird%characters[\t] -[Internationalized Stuff] -foo[bg]{0[1]} Bulgarian -foo{0[0]}Default -foo[en]{0[0]}English -foo[de]{0[0]}Deutsch -[Spaces] -key with spaces {0[1]} value -another with spaces {0[0]} splat! -""".format(self.delimiters, self.comment_prefixes) - if self.allow_no_value: - config_string += ( - "[NoValue]\n" - "option-without-value\n" - ) - - cf = self.fromstring(config_string) + def basic_test(self, cf): L = cf.sections() L.sort() E = ['Commented Bar', @@ -137,6 +101,95 @@ eq(cf.get('Long Line', 'foo'), 'this line is much, much longer than my editor\nlikes it.') + def test_basic(self): + config_string = """\ +[Foo Bar] +foo{0[0]}bar +[Spacey Bar] +foo {0[0]} bar +[Spacey Bar From The Beginning] + foo {0[0]} bar + baz {0[0]} qwe +[Commented Bar] +foo{0[1]} bar {1[1]} comment +baz{0[0]}qwe {1[0]}another one +[Long Line] +foo{0[1]} this line is much, much longer than my editor + likes it. +[Section\\with$weird%characters[\t] +[Internationalized Stuff] +foo[bg]{0[1]} Bulgarian +foo{0[0]}Default +foo[en]{0[0]}English +foo[de]{0[0]}Deutsch +[Spaces] +key with spaces {0[1]} value +another with spaces {0[0]} splat! +""".format(self.delimiters, self.comment_prefixes) + if self.allow_no_value: + config_string += ( + "[NoValue]\n" + "option-without-value\n" + ) + cf = self.fromstring(config_string) + self.basic_test(cf) + with self.assertRaises(configparser.DuplicateOptionError): + cf.readstring(textwrap.dedent("""\ + [Duplicate Options Here] + option {0[0]} with a value + option {0[1]} with another value + """.format(self.delimiters))) + + def test_basic_from_dict(self): + config = { + "Foo Bar": { + "foo": "bar", + }, + "Spacey Bar": { + "foo": "bar", + }, + "Spacey Bar From The Beginning": { + "foo": "bar", + "baz": "qwe", + }, + "Commented Bar": { + "foo": "bar", + "baz": "qwe", + }, + "Long Line": { + "foo": "this line is much, much longer than my editor\nlikes " + "it.", + }, + "Section\\with$weird%characters[\t": { + }, + "Internationalized Stuff": { + "foo[bg]": "Bulgarian", + "foo": "Default", + "foo[en]": "English", + "foo[de]": "Deutsch", + }, + "Spaces": { + "key with spaces": "value", + "another with spaces": "splat!", + } + } + if self.allow_no_value: + config.update({ + "NoValue": { + "option-without-value": None, + } + }) + cf = self.newconfig() + cf.readdict(config) + self.basic_test(cf) + with self.assertRaises(configparser.DuplicateOptionError): + cf.readdict({ + "Duplicate Options Here": { + 'option': 'with a value', + 'OPTION': 'with another value', + } + }) + def test_case_sensitivity(self): cf = self.newconfig() cf.add_section("A") @@ -185,25 +238,25 @@ "could not locate option, expecting case-insensitive defaults") def test_parse_errors(self): - self.newconfig() - self.parse_error(configparser.ParsingError, + cf = self.newconfig() + self.parse_error(cf, configparser.ParsingError, "[Foo]\n" "{}val-without-opt-name\n".format(self.delimiters[0])) - self.parse_error(configparser.ParsingError, + self.parse_error(cf, configparser.ParsingError, "[Foo]\n" "{}val-without-opt-name\n".format(self.delimiters[1])) - e = self.parse_error(configparser.MissingSectionHeaderError, + e = self.parse_error(cf, configparser.MissingSectionHeaderError, "No Section!\n") self.assertEqual(e.args, ('', 1, "No Section!\n")) if not self.allow_no_value: - e = self.parse_error(configparser.ParsingError, + e = self.parse_error(cf, configparser.ParsingError, "[Foo]\n wrong-indent\n") self.assertEqual(e.args, ('',)) - def parse_error(self, exc, src): + def parse_error(self, cf, exc, src): sio = io.StringIO(src) with self.assertRaises(exc) as cm: - self.cf.readfp(sio) + cf.readfp(sio) return cm.exception def test_query_errors(self): @@ -217,15 +270,15 @@ cf.options("Foo") with self.assertRaises(configparser.NoSectionError): cf.set("foo", "bar", "value") - e = self.get_error(configparser.NoSectionError, "foo", "bar") + e = self.get_error(cf, configparser.NoSectionError, "foo", "bar") self.assertEqual(e.args, ("foo",)) cf.add_section("foo") - e = self.get_error(configparser.NoOptionError, "foo", "bar") + e = self.get_error(cf, configparser.NoOptionError, "foo", "bar") self.assertEqual(e.args, ("bar", "foo")) - def get_error(self, exc, section, option): + def get_error(self, cf, exc, section, option): try: - self.cf.get(section, option) + cf.get(section, option) except exc as e: return e else: @@ -409,7 +462,7 @@ "something with lots of interpolation (9 steps)") eq(cf.get("Foo", "bar10"), "something with lots of interpolation (10 steps)") - e = self.get_error(configparser.InterpolationDepthError, "Foo", "bar11") + e = self.get_error(cf, configparser.InterpolationDepthError, "Foo", "bar11") self.assertEqual(e.args, ("bar11", "Foo", rawval[self.config_class])) def test_interpolation_missing_value(self): @@ -417,8 +470,8 @@ configparser.ConfigParser: '%(reference)s', configparser.SafeConfigParser: '', } - self.get_interpolation_config() - e = self.get_error(configparser.InterpolationMissingOptionError, + cf = self.get_interpolation_config() + e = self.get_error(cf, configparser.InterpolationMissingOptionError, "Interpolation Error", "name") self.assertEqual(e.reference, "reference") self.assertEqual(e.section, "Interpolation Error") @@ -645,15 +698,15 @@ dict_type = SortedDict def test_sorted(self): - self.fromstring("[b]\n" - "o4=1\n" - "o3=2\n" - "o2=3\n" - "o1=4\n" - "[a]\n" - "k=v\n") + cf = self.fromstring("[b]\n" + "o4=1\n" + "o3=2\n" + "o2=3\n" + "o1=4\n" + "[a]\n" + "k=v\n") output = io.StringIO() - self.cf.write(output) + cf.write(output) self.assertEquals(output.getvalue(), "[a]\n" "k = v\n\n"