Index: Lib/configparser.py =================================================================== --- Lib/configparser.py (revision 83295) +++ Lib/configparser.py (working copy) @@ -114,6 +114,7 @@ # fallback for setup.py which hasn't yet built _collections _default_dict = dict +from _abcoll import MutableMapping import re import sys @@ -261,7 +262,7 @@ self.args = (filename, lineno, line) -class RawConfigParser: +class RawConfigParser(MutableMapping): """ConfigParser that does not do interpolation.""" # Regular expressions for parsing section headers and options @@ -307,6 +308,8 @@ self._dict = dict_type self._sections = self._dict() self._defaults = self._dict() + self._views = self._dict() + self._views[DEFAULTSECT] = SectionView(self, DEFAULTSECT) if defaults: for key, value in defaults.items(): self._defaults[self.optionxform(key)] = value @@ -344,12 +347,13 @@ already exists. Raise ValueError if name is DEFAULT or any of it's case-insensitive variants. """ - if section.lower() == "default": + if section.upper() == DEFAULTSECT: raise ValueError('Invalid section name: %s' % section) if section in self._sections: raise DuplicateSectionError(section) self._sections[section] = self._dict() + self._views[section] = SectionView(self, section) def has_section(self, section): """Indicate whether the named section is present in the configuration. @@ -533,8 +537,37 @@ existed = section in self._sections if existed: del self._sections[section] + del self._views[section] return existed + def __getitem__(self, key): + if key != DEFAULTSECT and not self.has_section(key): + raise KeyError(key) + return self._views[key] + + def __setitem__(self, key, value): + raise NotImplementedError("Design decision needed.") + + def __delitem__(self, key): + if key == DEFAULTSECT: + raise ValueError("Cannot remove the default section.") + if not self.has_section(key): + raise KeyError(key) + self.remove_section(key) + + def __contains__(self, key): + return key == DEFAULTSECT or self.has_section(key) + + def __len__(self): + return len(self._sections) + 1 # the default section + + def __iter__(self): + # XXX this could use itertools.chain if it was built into the executable + # XXX does not break when underlying container state changed + all_sections = [DEFAULTSECT] + all_sections.extend(self._sections.keys()) + return all_sections.__iter__() + def _read(self, fp, fpname): """Parse a sectioned configuration file. @@ -605,6 +638,7 @@ cursect = self._dict() cursect['__name__'] = sectname self._sections[sectname] = cursect + self._views[sectname] = SectionView(self, sectname) # So sections can't start with a continuation line optname = None # no section header in the file? @@ -641,6 +675,7 @@ self._join_multiline_values() def _join_multiline_values(self): + # XXX this could use itertools.chain if it was built into the executable all_sections = [self._defaults] all_sections.extend(self._sections.values()) for options in all_sections: @@ -821,3 +856,53 @@ raise ValueError("invalid interpolation syntax in %r at " "position %d" % (value, percent_index)) ConfigParser.set(self, section, option, value) + + +class SectionView(MutableMapping): + """A mutable view on a single section from a parser.""" + + _noname = ("__name__ special key access and modification " + "not supported through the mapping interface.") + + def __init__(self, parser, section_name): + """Creates a view on a section named `section_name` in `parser`.""" + self._parser = parser + self._section = section_name + + def __repr__(self): + return ''.format(self._section) + + def __getitem__(self, key): + if key == '__name__': + raise ValueError(self._noname) + if not self._parser.has_option(self._section, key): + raise KeyError(key) + return self._parser.get(self._section, key) + + def __setitem__(self, key, value): + if key == '__name__': + raise ValueError(self._noname) + return self._parser.set(self._section, key, value) + + def __delitem__(self, key): + if key == '__name__': + raise ValueError(self._noname) + if not self._parser.has_option(self._section, key): + raise KeyError(key) + return self._parser.remove_option(self._section, key) + + def __contains__(self, key): + if key == '__name__': + return False + return self._parser.has_option(self._section, key) + + def __len__(self): + # __name__ is properly hidden by .options() + # XXX weak performance + return len(self._parser.options(self._section)) + + def __iter__(self): + # __name__ is properly hidden by .options() + # XXX weak performance + # XXX does not break when underlying container state changed + return self._parser.options(self._section).__iter__() Index: Lib/test/test_cfgparser.py =================================================================== --- Lib/test/test_cfgparser.py (revision 83295) +++ Lib/test/test_cfgparser.py (working copy) @@ -57,14 +57,14 @@ def test_basic(self): config_string = """\ [Foo Bar] -foo{0[0]}bar +foo{0[0]}bar1 [Spacey Bar] -foo {0[0]} bar +foo {0[0]} bar2 [Spacey Bar From The Beginning] - foo {0[0]} bar + foo {0[0]} bar3 baz {0[0]} qwe [Commented Bar] -foo{0[1]} bar {1[1]} comment +foo{0[1]} bar4 {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 @@ -86,8 +86,6 @@ ) cf = self.fromstring(config_string) - L = cf.sections() - L.sort() E = ['Commented Bar', 'Foo Bar', 'Internationalized Stuff', @@ -97,31 +95,74 @@ 'Spacey Bar', 'Spacey Bar From The Beginning', ] + + # API access + L = cf.sections() + L.sort() if self.allow_no_value: E.append(r'NoValue') E.sort() eq = self.assertEqual eq(L, E) + # mapping access + L = [section for section in cf] + L.sort() + E.append(configparser.DEFAULTSECT) + E.sort() + eq(L, E) + # The use of spaces in the section names serves as a # regression test for SourceForge bug #583248: # http://www.python.org/sf/583248 - eq(cf.get('Foo Bar', 'foo'), 'bar') - eq(cf.get('Spacey Bar', 'foo'), 'bar') - eq(cf.get('Spacey Bar From The Beginning', 'foo'), 'bar') + # API access + eq(cf.get('Foo Bar', 'foo'), 'bar1') + eq(cf.get('Spacey Bar', 'foo'), 'bar2') + eq(cf.get('Spacey Bar From The Beginning', 'foo'), 'bar3') eq(cf.get('Spacey Bar From The Beginning', 'baz'), 'qwe') - eq(cf.get('Commented Bar', 'foo'), 'bar') + eq(cf.get('Commented Bar', 'foo'), 'bar4') eq(cf.get('Commented Bar', 'baz'), 'qwe') eq(cf.get('Spaces', 'key with spaces'), 'value') eq(cf.get('Spaces', 'another with spaces'), 'splat!') + eq(cf.get('Long Line', 'foo'), + 'this line is much, much longer than my editor\nlikes it.') if self.allow_no_value: eq(cf.get('NoValue', 'option-without-value'), None) + # mapping access + eq(cf['Foo Bar']['foo'], 'bar1') + eq(cf['Spacey Bar']['foo'], 'bar2') + eq(cf['Spacey Bar From The Beginning']['foo'], 'bar3') + eq(cf['Spacey Bar From The Beginning']['baz'], 'qwe') + eq(cf['Commented Bar']['foo'], 'bar4') + eq(cf['Commented Bar']['baz'], 'qwe') + eq(cf['Spaces']['key with spaces'], 'value') + eq(cf['Spaces']['another with spaces'], 'splat!') + eq(cf['Long Line']['foo'], + 'this line is much, much longer than my editor\nlikes it.') + if self.allow_no_value: + eq(cf['NoValue']['option-without-value'], None) + + # API access self.assertNotIn('__name__', cf.options("Foo Bar"), '__name__ "option" should not be exposed by the API!') + # mapping access + self.assertNotIn('__name__', cf['Foo Bar'], + '__name__ "option" should not be exposed by ' + 'mapping protocol access') + self.assertFalse('__name__' in cf['Foo Bar']) + with self.assertRaises(ValueError): + cf['Foo Bar']['__name__'] + with self.assertRaises(ValueError): + del cf['Foo Bar']['__name__'] + with self.assertRaises(ValueError): + cf['Foo Bar']['__name__'] = "can't write to this special name" + # Make sure the right things happen for remove_option(); # added to include check for SourceForge bug #123324: + + # API access self.assertTrue(cf.remove_option('Foo Bar', 'foo'), "remove_option() failed to report existence of option") self.assertFalse(cf.has_option('Foo Bar', 'foo'), @@ -129,14 +170,19 @@ self.assertFalse(cf.remove_option('Foo Bar', 'foo'), "remove_option() failed to report non-existence of option" " that was removed") - 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.') + # mapping access + del cf['Spacey Bar']['foo'] + self.assertFalse('foo' in cf['Spacey Bar']) + with self.assertRaises(KeyError): + del cf['Spacey Bar']['foo'] + with self.assertRaises(KeyError): + del cf['No Such Section']['foo'] + def test_case_sensitivity(self): cf = self.newconfig() cf.add_section("A")