diff -r 1c6cf4010df3 Doc/library/configparser.rst --- a/Doc/library/configparser.rst Thu May 05 11:14:06 2016 +0300 +++ b/Doc/library/configparser.rst Fri May 06 18:36:49 2016 +0800 @@ -384,6 +384,10 @@ "a" in parser["section"] "A" in parser["section"] +* All section names including ``DEFAULTSECT`` are passed through + :meth:`sectionxform` [1]_. This does nothing but merely returns the section + name to preserve backward compatibility. + * All sections include ``DEFAULTSECT`` values as well which means that ``.clear()`` on a section may not leave the section visibly empty. This is because default values cannot be deleted from the section (because technically @@ -740,6 +744,39 @@ >>> list(custom['Section2'].keys()) ['AnotherKey'] +.. method:: sectionxform(section) + + This method transforms section names on every read, get or set operation. + The default merely returns *section*. You can customize section names by + overriding this method. + For example: + + .. doctest:: + + >>> config = """ + ... [Section] + ... key = value + ... """ + >>> typical = configparser.ConfigParser() + >>> typical.read_string(config) + >>> typical.has_section('Section') + True + >>> typical.has_section('section') + False + >>> typical['section'] + Traceback (most recent call last): + ... + KeyError: 'section' + >>> custom = configparser.ConfigParser() + >>> custom.sectionxform = lambda sect: sect.lower() + >>> custom.read_string(config) + >>> custom.has_section('Section') + True + >>> custom.has_section('section') + True + >>> custom['section'] + + .. attribute:: SECTCRE A compiled regular expression used to parse section headers. The default @@ -1150,6 +1187,15 @@ names is stripped before :meth:`optionxform` is called. + .. method:: sectionxform(section) + + Hook provided to transform the section names as :meth:`optionxform` does + for option names. The default implementation does nothing but merely + returns *section*. + + .. versionadded:: 3.6 + + .. method:: readfp(fp, filename=None) .. deprecated:: 3.2 diff -r 1c6cf4010df3 Lib/configparser.py --- a/Lib/configparser.py Thu May 05 11:14:06 2016 +0300 +++ b/Lib/configparser.py Fri May 06 18:36:49 2016 +0800 @@ -494,7 +494,7 @@ opt = parser.optionxform(path[0]) v = map[opt] elif len(path) == 2: - sect = path[0] + sect = parser.sectionxform(path[0]) opt = parser.optionxform(path[1]) v = parser.get(sect, opt, raw=True) else: @@ -608,6 +608,7 @@ self._defaults = self._dict() self._converters = ConverterMapping(self) self._proxies = self._dict() + default_section = self.sectionxform(default_section) self._proxies[default_section] = SectionProxy(self, default_section) if defaults: for key, value in defaults.items(): @@ -651,6 +652,7 @@ Raise DuplicateSectionError if a section by the specified name already exists. Raise ValueError if name is DEFAULT. """ + section = self.sectionxform(section) if section == self.default_section: raise ValueError('Invalid section name: %r' % section) @@ -664,11 +666,12 @@ The DEFAULT section is not acknowledged. """ - return section in self._sections + return self.sectionxform(section) in self._sections def options(self, section): """Return a list of option names for the given section name.""" try: + section = self.sectionxform(section) opts = self._sections[section].copy() except KeyError: raise NoSectionError(section) from None @@ -734,7 +737,7 @@ """ elements_added = set() for section, keys in dictionary.items(): - section = str(section) + section = self.sectionxform(str(section)) try: self.add_section(section) except (DuplicateSectionError, ValueError): @@ -774,6 +777,7 @@ The section DEFAULT is special. """ + section = self.sectionxform(section) try: d = self._unify_values(section, vars) except NoSectionError: @@ -838,6 +842,7 @@ """ if section is _UNSET: return super().items() + section = self.sectionxform(section) d = self._defaults.copy() try: d.update(self._sections[section]) @@ -870,10 +875,14 @@ def optionxform(self, optionstr): return optionstr.lower() + def sectionxform(self, sectionstr): + return sectionstr + def has_option(self, section, option): """Check for the existence of a given option in a given section. If the specified `section' is None or an empty string, DEFAULT is assumed. If the specified `section' does not exist, returns False.""" + section = self.sectionxform(section) if not section or section == self.default_section: option = self.optionxform(option) return option in self._defaults @@ -886,6 +895,8 @@ def set(self, section, option, value=None): """Set an option.""" + option = self.optionxform(option) + section = self.sectionxform(section) if value: value = self._interpolation.before_set(self, section, option, value) @@ -896,7 +907,7 @@ sectdict = self._sections[section] except KeyError: raise NoSectionError(section) from None - sectdict[self.optionxform(option)] = value + sectdict[option] = value def write(self, fp, space_around_delimiters=True): """Write an .ini-format representation of the configuration state. @@ -930,6 +941,7 @@ def remove_option(self, section, option): """Remove an option.""" + section = self.sectionxform(section) if not section or section == self.default_section: sectdict = self._defaults else: @@ -945,6 +957,7 @@ def remove_section(self, section): """Remove a file section.""" + section = self.sectionxform(section) existed = section in self._sections if existed: del self._sections[section] @@ -952,6 +965,7 @@ return existed def __getitem__(self, key): + key = self.sectionxform(key) if key != self.default_section and not self.has_section(key): raise KeyError(key) return self._proxies[key] @@ -962,6 +976,7 @@ # XXX this is not atomic if read_dict fails at any point. Then again, # no update method in configparser is atomic in this implementation. + key = self.sectionxform(key) if key == self.default_section: self._defaults.clear() elif key in self._sections: @@ -969,6 +984,7 @@ self.read_dict({key: value}) def __delitem__(self, key): + key = self.sectionxform(key) if key == self.default_section: raise ValueError("Cannot remove the default section.") if not self.has_section(key): @@ -976,6 +992,7 @@ self.remove_section(key) def __contains__(self, key): + key = self.sectionxform(key) return key == self.default_section or self.has_section(key) def __len__(self): @@ -1056,7 +1073,7 @@ # is it a section header? mo = self.SECTCRE.match(value) if mo: - sectname = mo.group('header') + sectname = self.sectionxform(mo.group('header')) if sectname in self._sections: if self._strict and sectname in elements_added: raise DuplicateSectionError(sectname, fpname, diff -r 1c6cf4010df3 Lib/test/test_configparser.py --- a/Lib/test/test_configparser.py Thu May 05 11:14:06 2016 +0300 +++ b/Lib/test/test_configparser.py Fri May 06 18:36:49 2016 +0800 @@ -1090,10 +1090,12 @@ default_section = 'common' strict = True - def fromstring(self, string, defaults=None, optionxform=None): + def fromstring(self, string, defaults=None, optionxform=None, sectionxform=None): cf = self.newconfig(defaults) if optionxform: cf.optionxform = optionxform + if sectionxform: + cf.sectionxform = sectionxform cf.read_string(string) return cf @@ -1231,6 +1233,29 @@ eq(cf['random']['foo'], 'value redefined') eq(cf['random']['Foo'], 'A Better Value Redefined') + def test_sectionxform(self): + ini = textwrap.dedent(""" + [Test] + option = value + """).strip() + cf = self.fromstring(ini, sectionxform=lambda sect: sect.lower()) + with self.assertRaises(ValueError): + cf.add_section('COMMON') + with self.assertRaises(configparser.DuplicateSectionError): + cf.add_section('test') + with self.assertRaises(configparser.DuplicateSectionError): + cf.read_dict({ + 'test': {'option1': 'value1'}, + 'TEST': {'option2': 'value2'} + }) + cf.set('TEST', 'key', 'value') + self.assertTrue(cf.has_section('TEST')) + self.assertTrue(cf.has_section('test')) + self.assertEqual(cf.get('TEST', 'option'), 'value') + self.assertEqual(cf.get('test', 'option'), 'value') + self.assertEqual(cf.get('Test', 'key'), 'value') + self.assertEqual(cf.get('test', 'key'), 'value') + def test_other_errors(self): cf = self.fromstring(""" [interpolation fail]