Issue #5412: extend configparser to support mapping access
diff --git a/Lib/configparser.py b/Lib/configparser.py
index 03d6713..80f6aed 100644
--- a/Lib/configparser.py
+++ b/Lib/configparser.py
@@ -85,7 +85,7 @@
         and their keys will be added in order. Values are automatically
         converted to strings.
 
-    get(section, option, raw=False, vars=None, default=_UNSET)
+    get(section, option, raw=False, vars=None, fallback=_UNSET)
         Return a string value for the named option.  All % interpolations are
         expanded in the return values, based on the defaults passed into the
         constructor and the DEFAULT section.  Additional substitutions may be
@@ -93,13 +93,13 @@
         contents override any pre-existing defaults. If `option' is a key in
         `vars', the value from `vars' is used.
 
-    getint(section, options, raw=False, vars=None, default=_UNSET)
+    getint(section, options, raw=False, vars=None, fallback=_UNSET)
         Like get(), but convert value to an integer.
 
-    getfloat(section, options, raw=False, vars=None, default=_UNSET)
+    getfloat(section, options, raw=False, vars=None, fallback=_UNSET)
         Like get(), but convert value to a float.
 
-    getboolean(section, options, raw=False, vars=None, default=_UNSET)
+    getboolean(section, options, raw=False, vars=None, fallback=_UNSET)
         Like get(), but convert value to a boolean (currently case
         insensitively defined as 0, false, no, off for False, and 1, true,
         yes, on for True).  Returns False or True.
@@ -123,13 +123,10 @@
         between keys and values are surrounded by spaces.
 """
 
-try:
-    from collections import OrderedDict as _default_dict
-except ImportError:
-    # fallback for setup.py which hasn't yet built _collections
-    _default_dict = dict
-
+from collections import MutableMapping, OrderedDict as _default_dict
+import functools
 import io
+import itertools
 import re
 import sys
 import warnings
@@ -366,7 +363,7 @@
 _UNSET = object()
 
 
-class RawConfigParser:
+class RawConfigParser(MutableMapping):
     """ConfigParser that does not do interpolation."""
 
     # Regular expressions for parsing section headers and options
@@ -413,6 +410,8 @@
         self._dict = dict_type
         self._sections = self._dict()
         self._defaults = self._dict()
+        self._views = self._dict()
+        self._views[DEFAULTSECT] = SectionProxy(self, DEFAULTSECT)
         if defaults:
             for key, value in defaults.items():
                 self._defaults[self.optionxform(key)] = value
@@ -434,6 +433,7 @@
             self._startonly_comment_prefixes = ()
             self._comment_prefixes = tuple(comment_prefixes or ())
         self._strict = strict
+        self._allow_no_value = allow_no_value
         self._empty_lines_in_values = empty_lines_in_values
 
     def defaults(self):
@@ -451,12 +451,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] = SectionProxy(self, section)
 
     def has_section(self, section):
         """Indicate whether the named section is present in the configuration.
@@ -534,7 +535,7 @@
         for section, keys in dictionary.items():
             try:
                 self.add_section(section)
-            except DuplicateSectionError:
+            except (DuplicateSectionError, ValueError):
                 if self._strict and section in elements_added:
                     raise
                 elements_added.add(section)
@@ -556,29 +557,31 @@
         )
         self.read_file(fp, source=filename)
 
-    def get(self, section, option, vars=None, default=_UNSET):
+    def get(self, section, option, *, vars=None, fallback=_UNSET):
         """Get an option value for a given section.
 
         If `vars' is provided, it must be a dictionary. The option is looked up
         in `vars' (if provided), `section', and in `DEFAULTSECT' in that order.
-        If the key is not found and `default' is provided, it is used as
-        a fallback value. `None' can be provided as a `default' value.
+        If the key is not found and `fallback' is provided, it is used as
+        a fallback value. `None' can be provided as a `fallback' value.
+
+        Arguments `vars' and `fallback' are keyword only.
         """
         try:
             d = self._unify_values(section, vars)
         except NoSectionError:
-            if default is _UNSET:
+            if fallback is _UNSET:
                 raise
             else:
-                return default
+                return fallback
         option = self.optionxform(option)
         try:
             return d[option]
         except KeyError:
-            if default is _UNSET:
+            if fallback is _UNSET:
                 raise NoOptionError(option, section)
             else:
-                return default
+                return fallback
 
     def items(self, section):
         try:
@@ -593,35 +596,36 @@
             del d["__name__"]
         return d.items()
 
-    def _get(self, section, conv, option, *args, **kwargs):
-        return conv(self.get(section, option, *args, **kwargs))
+    def _get(self, section, conv, option, **kwargs):
+        return conv(self.get(section, option, **kwargs))
 
-    def getint(self, section, option, vars=None, default=_UNSET):
+    def getint(self, section, option, *, vars=None, fallback=_UNSET):
         try:
-            return self._get(section, int, option, vars)
+            return self._get(section, int, option, vars=vars)
         except (NoSectionError, NoOptionError):
-            if default is _UNSET:
+            if fallback is _UNSET:
                 raise
             else:
-                return default
+                return fallback
 
-    def getfloat(self, section, option, vars=None, default=_UNSET):
+    def getfloat(self, section, option, *, vars=None, fallback=_UNSET):
         try:
-            return self._get(section, float, option, vars)
+            return self._get(section, float, option, vars=vars)
         except (NoSectionError, NoOptionError):
-            if default is _UNSET:
+            if fallback is _UNSET:
                 raise
             else:
-                return default
+                return fallback
 
-    def getboolean(self, section, option, vars=None, default=_UNSET):
+    def getboolean(self, section, option, *, vars=None, fallback=_UNSET):
         try:
-            return self._get(section, self._convert_to_boolean, option, vars)
+            return self._get(section, self._convert_to_boolean, option,
+                             vars=vars)
         except (NoSectionError, NoOptionError):
-            if default is _UNSET:
+            if fallback is _UNSET:
                 raise
             else:
-                return default
+                return fallback
 
     def optionxform(self, optionstr):
         return optionstr.lower()
@@ -671,7 +675,7 @@
         for key, value in section_items:
             if key == "__name__":
                 continue
-            if (value is not None) or (self._optcre == self.OPTCRE):
+            if value is not None or not self._allow_no_value:
                 value = delimiter + str(value).replace('\n', '\n\t')
             else:
                 value = ""
@@ -698,8 +702,40 @@
         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):
+        # To conform with the mapping protocol, overwrites existing values in
+        # the section.
+
+        # XXX this is not atomic if read_dict fails at any point. Then again,
+        # no update method in configparser is atomic in this implementation.
+        self.remove_section(key)
+        self.read_dict({key: value})
+
+    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 does it break when underlying container state changed?
+        return itertools.chain((DEFAULTSECT,), self._sections.keys())
+
     def _read(self, fp, fpname):
         """Parse a sectioned configuration file.
 
@@ -776,6 +812,7 @@
                         cursect = self._dict()
                         cursect['__name__'] = sectname
                         self._sections[sectname] = cursect
+                        self._views[sectname] = SectionProxy(self, sectname)
                         elements_added.add(sectname)
                     # So sections can't start with a continuation line
                     optname = None
@@ -818,8 +855,8 @@
         self._join_multiline_values()
 
     def _join_multiline_values(self):
-        all_sections = [self._defaults]
-        all_sections.extend(self._sections.values())
+        all_sections = itertools.chain((self._defaults,),
+                                       self._sections.values())
         for options in all_sections:
             for name, val in options.items():
                 if isinstance(val, list):
@@ -857,73 +894,95 @@
             raise ValueError('Not a boolean: %s' % value)
         return self.BOOLEAN_STATES[value.lower()]
 
+    def _validate_value_type(self, value):
+        """Raises a TypeError for non-string values.
+
+        The only legal non-string value if we allow valueless
+        options is None, so we need to check if the value is a
+        string if:
+        - we do not allow valueless options, or
+        - we allow valueless options but the value is not None
+
+        For compatibility reasons this method is not used in classic set()
+        for RawConfigParsers and ConfigParsers. It is invoked in every
+        case for mapping protocol access and in SafeConfigParser.set().
+        """
+        if not self._allow_no_value or value:
+            if not isinstance(value, str):
+                raise TypeError("option values must be strings")
+
+
 
 class ConfigParser(RawConfigParser):
     """ConfigParser implementing interpolation."""
 
-    def get(self, section, option, raw=False, vars=None, default=_UNSET):
+    def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
         """Get an option value for a given section.
 
         If `vars' is provided, it must be a dictionary. The option is looked up
         in `vars' (if provided), `section', and in `DEFAULTSECT' in that order.
-        If the key is not found and `default' is provided, it is used as
-        a fallback value. `None' can be provided as a `default' value.
+        If the key is not found and `fallback' is provided, it is used as
+        a fallback value. `None' can be provided as a `fallback' value.
 
         All % interpolations are expanded in the return values, unless the
         optional argument `raw' is true.  Values for interpolation keys are
         looked up in the same manner as the option.
 
+        Arguments `raw', `vars', and `fallback' are keyword only.
+
         The section DEFAULT is special.
         """
         try:
             d = self._unify_values(section, vars)
         except NoSectionError:
-            if default is _UNSET:
+            if fallback is _UNSET:
                 raise
             else:
-                return default
+                return fallback
         option = self.optionxform(option)
         try:
             value = d[option]
         except KeyError:
-            if default is _UNSET:
+            if fallback is _UNSET:
                 raise NoOptionError(option, section)
             else:
-                return default
+                return fallback
 
         if raw or value is None:
             return value
         else:
             return self._interpolate(section, option, value, d)
 
-    def getint(self, section, option, raw=False, vars=None, default=_UNSET):
+    def getint(self, section, option, *, raw=False, vars=None,
+               fallback=_UNSET):
         try:
-            return self._get(section, int, option, raw, vars)
+            return self._get(section, int, option, raw=raw, vars=vars)
         except (NoSectionError, NoOptionError):
-            if default is _UNSET:
+            if fallback is _UNSET:
                 raise
             else:
-                return default
+                return fallback
 
-    def getfloat(self, section, option, raw=False, vars=None, default=_UNSET):
+    def getfloat(self, section, option, *, raw=False, vars=None,
+                 fallback=_UNSET):
         try:
-            return self._get(section, float, option, raw, vars)
+            return self._get(section, float, option, raw=raw, vars=vars)
         except (NoSectionError, NoOptionError):
-            if default is _UNSET:
+            if fallback is _UNSET:
                 raise
             else:
-                return default
+                return fallback
 
-    def getboolean(self, section, option, raw=False, vars=None,
-                   default=_UNSET):
+    def getboolean(self, section, option, *, raw=False, vars=None,
+                   fallback=_UNSET):
         try:
-            return self._get(section, self._convert_to_boolean, option, raw,
-                             vars)
+            return self._get(section, self._convert_to_boolean, option,
+                             raw=raw, vars=vars)
         except (NoSectionError, NoOptionError):
-            if default is _UNSET:
+            if fallback is _UNSET:
                 raise
             else:
-                return default
+                return fallback
 
     def items(self, section, raw=False, vars=None):
         """Return a list of (name, value) tuples for each option in a section.
@@ -1037,14 +1096,7 @@
 
     def set(self, section, option, value=None):
         """Set an option.  Extend ConfigParser.set: check for string values."""
-        # The only legal non-string value if we allow valueless
-        # options is None, so we need to check if the value is a
-        # string if:
-        # - we do not allow valueless options, or
-        # - we allow valueless options but the value is not None
-        if self._optcre is self.OPTCRE or value:
-            if not isinstance(value, str):
-                raise TypeError("option values must be strings")
+        self._validate_value_type(value)
         # check for bad percent signs
         if value:
             tmp_value = value.replace('%%', '') # escaped percent signs
@@ -1053,3 +1105,60 @@
                 raise ValueError("invalid interpolation syntax in %r at "
                                 "position %d" % (value, tmp_value.find('%')))
         ConfigParser.set(self, section, option, value)
+
+
+class SectionProxy(MutableMapping):
+    """A proxy for 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
+        self.getint = functools.partial(self._parser.getint,
+                                        self._section)
+        self.getfloat = functools.partial(self._parser.getfloat,
+                                          self._section)
+        self.getboolean = functools.partial(self._parser.getboolean,
+                                            self._section)
+
+    def __repr__(self):
+        return '<Section: {}>'.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)
+        self._parser._validate_value_type(value)
+        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__()
diff --git a/Lib/logging/config.py b/Lib/logging/config.py
index 12c1c13..5afdf9f 100644
--- a/Lib/logging/config.py
+++ b/Lib/logging/config.py
@@ -103,7 +103,7 @@
 
 def _create_formatters(cp):
     """Create and return formatters"""
-    flist = cp.get("formatters", "keys")
+    flist = cp["formatters"]["keys"]
     if not len(flist):
         return {}
     flist = flist.split(",")
@@ -111,20 +111,12 @@
     formatters = {}
     for form in flist:
         sectname = "formatter_%s" % form
-        opts = cp.options(sectname)
-        if "format" in opts:
-            fs = cp.get(sectname, "format", 1)
-        else:
-            fs = None
-        if "datefmt" in opts:
-            dfs = cp.get(sectname, "datefmt", 1)
-        else:
-            dfs = None
+        fs = cp.get(sectname, "format", raw=True, fallback=None)
+        dfs = cp.get(sectname, "datefmt", raw=True, fallback=None)
         c = logging.Formatter
-        if "class" in opts:
-            class_name = cp.get(sectname, "class")
-            if class_name:
-                c = _resolve(class_name)
+        class_name = cp[sectname].get("class")
+        if class_name:
+            c = _resolve(class_name)
         f = c(fs, dfs)
         formatters[form] = f
     return formatters
@@ -132,7 +124,7 @@
 
 def _install_handlers(cp, formatters):
     """Install and return handlers"""
-    hlist = cp.get("handlers", "keys")
+    hlist = cp["handlers"]["keys"]
     if not len(hlist):
         return {}
     hlist = hlist.split(",")
@@ -140,30 +132,23 @@
     handlers = {}
     fixups = [] #for inter-handler references
     for hand in hlist:
-        sectname = "handler_%s" % hand
-        klass = cp.get(sectname, "class")
-        opts = cp.options(sectname)
-        if "formatter" in opts:
-            fmt = cp.get(sectname, "formatter")
-        else:
-            fmt = ""
+        section = cp["handler_%s" % hand]
+        klass = section["class"]
+        fmt = section.get("formatter", "")
         try:
             klass = eval(klass, vars(logging))
         except (AttributeError, NameError):
             klass = _resolve(klass)
-        args = cp.get(sectname, "args")
+        args = section["args"]
         args = eval(args, vars(logging))
         h = klass(*args)
-        if "level" in opts:
-            level = cp.get(sectname, "level")
+        if "level" in section:
+            level = section["level"]
             h.setLevel(logging._levelNames[level])
         if len(fmt):
             h.setFormatter(formatters[fmt])
         if issubclass(klass, logging.handlers.MemoryHandler):
-            if "target" in opts:
-                target = cp.get(sectname,"target")
-            else:
-                target = ""
+            target = section.get("target", "")
             if len(target): #the target handler may not be loaded yet, so keep for later...
                 fixups.append((h, target))
         handlers[hand] = h
@@ -197,20 +182,19 @@
     """Create and install loggers"""
 
     # configure the root first
-    llist = cp.get("loggers", "keys")
+    llist = cp["loggers"]["keys"]
     llist = llist.split(",")
     llist = list(map(lambda x: x.strip(), llist))
     llist.remove("root")
-    sectname = "logger_root"
+    section = cp["logger_root"]
     root = logging.root
     log = root
-    opts = cp.options(sectname)
-    if "level" in opts:
-        level = cp.get(sectname, "level")
+    if "level" in section:
+        level = section["level"]
         log.setLevel(logging._levelNames[level])
     for h in root.handlers[:]:
         root.removeHandler(h)
-    hlist = cp.get(sectname, "handlers")
+    hlist = section["handlers"]
     if len(hlist):
         hlist = hlist.split(",")
         hlist = _strip_spaces(hlist)
@@ -237,13 +221,9 @@
     child_loggers = []
     #now set up the new ones...
     for log in llist:
-        sectname = "logger_%s" % log
-        qn = cp.get(sectname, "qualname")
-        opts = cp.options(sectname)
-        if "propagate" in opts:
-            propagate = cp.getint(sectname, "propagate")
-        else:
-            propagate = 1
+        section = cp["logger_%s" % log]
+        qn = section["qualname"]
+        propagate = section.getint("propagate", fallback=1)
         logger = logging.getLogger(qn)
         if qn in existing:
             i = existing.index(qn)
@@ -255,14 +235,14 @@
                 child_loggers.append(existing[i])
                 i = i + 1
             existing.remove(qn)
-        if "level" in opts:
-            level = cp.get(sectname, "level")
+        if "level" in section:
+            level = section["level"]
             logger.setLevel(logging._levelNames[level])
         for h in logger.handlers[:]:
             logger.removeHandler(h)
         logger.propagate = propagate
         logger.disabled = 0
-        hlist = cp.get(sectname, "handlers")
+        hlist = section["handlers"]
         if len(hlist):
             hlist = hlist.split(",")
             hlist = _strip_spaces(hlist)
diff --git a/Lib/test/test_cfgparser.py b/Lib/test/test_cfgparser.py
index f7e240f..11aa267 100644
--- a/Lib/test/test_cfgparser.py
+++ b/Lib/test/test_cfgparser.py
@@ -52,8 +52,6 @@
 class BasicTestCase(CfgParserTestCaseClass):
 
     def basic_test(self, cf):
-        L = cf.sections()
-        L.sort()
         E = ['Commented Bar',
              'Foo Bar',
              'Internationalized Stuff',
@@ -64,20 +62,34 @@
              'Spacey Bar From The Beginning',
              'Types',
              ]
+
         if self.allow_no_value:
             E.append('NoValue')
         E.sort()
+
+        # API access
+        L = cf.sections()
+        L.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!')
@@ -89,40 +101,69 @@
         if self.allow_no_value:
             eq(cf.get('NoValue', 'option-without-value'), None)
 
-        # test vars= and default=
-        eq(cf.get('Foo Bar', 'foo', default='baz'), 'bar')
+        # test vars= and fallback=
+        eq(cf.get('Foo Bar', 'foo', fallback='baz'), 'bar1')
         eq(cf.get('Foo Bar', 'foo', vars={'foo': 'baz'}), 'baz')
         with self.assertRaises(configparser.NoSectionError):
             cf.get('No Such Foo Bar', 'foo')
         with self.assertRaises(configparser.NoOptionError):
             cf.get('Foo Bar', 'no-such-foo')
-        eq(cf.get('No Such Foo Bar', 'foo', default='baz'), 'baz')
-        eq(cf.get('Foo Bar', 'no-such-foo', default='baz'), 'baz')
-        eq(cf.get('Spacey Bar', 'foo', default=None), 'bar')
-        eq(cf.get('No Such Spacey Bar', 'foo', default=None), None)
-        eq(cf.getint('Types', 'int', default=18), 42)
-        eq(cf.getint('Types', 'no-such-int', default=18), 18)
-        eq(cf.getint('Types', 'no-such-int', default="18"), "18") # sic!
+        eq(cf.get('No Such Foo Bar', 'foo', fallback='baz'), 'baz')
+        eq(cf.get('Foo Bar', 'no-such-foo', fallback='baz'), 'baz')
+        eq(cf.get('Spacey Bar', 'foo', fallback=None), 'bar2')
+        eq(cf.get('No Such Spacey Bar', 'foo', fallback=None), None)
+        eq(cf.getint('Types', 'int', fallback=18), 42)
+        eq(cf.getint('Types', 'no-such-int', fallback=18), 18)
+        eq(cf.getint('Types', 'no-such-int', fallback="18"), "18") # sic!
         self.assertAlmostEqual(cf.getfloat('Types', 'float',
-                                           default=0.0), 0.44)
+                                           fallback=0.0), 0.44)
         self.assertAlmostEqual(cf.getfloat('Types', 'no-such-float',
-                                           default=0.0), 0.0)
-        eq(cf.getfloat('Types', 'no-such-float', default="0.0"), "0.0") # sic!
-        eq(cf.getboolean('Types', 'boolean', default=True), False)
-        eq(cf.getboolean('Types', 'no-such-boolean', default="yes"),
+                                           fallback=0.0), 0.0)
+        eq(cf.getfloat('Types', 'no-such-float', fallback="0.0"), "0.0") # sic!
+        eq(cf.getboolean('Types', 'boolean', fallback=True), False)
+        eq(cf.getboolean('Types', 'no-such-boolean', fallback="yes"),
            "yes") # sic!
-        eq(cf.getboolean('Types', 'no-such-boolean', default=True), True)
-        eq(cf.getboolean('No Such Types', 'boolean', default=True), True)
+        eq(cf.getboolean('Types', 'no-such-boolean', fallback=True), True)
+        eq(cf.getboolean('No Such Types', 'boolean', fallback=True), True)
         if self.allow_no_value:
-            eq(cf.get('NoValue', 'option-without-value', default=False), None)
+            eq(cf.get('NoValue', 'option-without-value', fallback=False), None)
             eq(cf.get('NoValue', 'no-such-option-without-value',
-                      default=False), False)
+                      fallback=False), False)
 
+        # 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 acceess
         self.assertTrue(cf.remove_option('Foo Bar', 'foo'),
                         "remove_option() failed to report existence of option")
         self.assertFalse(cf.has_option('Foo Bar', 'foo'),
@@ -138,17 +179,25 @@
         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_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
@@ -205,17 +254,17 @@
     def test_basic_from_dict(self):
         config = {
             "Foo Bar": {
-                "foo": "bar",
+                "foo": "bar1",
             },
             "Spacey Bar": {
-                "foo": "bar",
+                "foo": "bar2",
             },
             "Spacey Bar From The Beginning": {
-                "foo": "bar",
+                "foo": "bar3",
                 "baz": "qwe",
             },
             "Commented Bar": {
-                "foo": "bar",
+                "foo": "bar4",
                 "baz": "qwe",
             },
             "Long Line": {
@@ -270,14 +319,18 @@
         cf = self.newconfig()
         cf.add_section("A")
         cf.add_section("a")
+        cf.add_section("B")
         L = cf.sections()
         L.sort()
         eq = self.assertEqual
-        eq(L, ["A", "a"])
+        eq(L, ["A", "B", "a"])
         cf.set("a", "B", "value")
         eq(cf.options("a"), ["b"])
         eq(cf.get("a", "b"), "value",
            "could not locate option, expecting case-insensitive option names")
+        with self.assertRaises(configparser.NoSectionError):
+            # section names are case-sensitive
+            cf.set("b", "A", "value")
         self.assertTrue(cf.has_option("a", "b"))
         cf.set("A", "A-B", "A-B value")
         for opt in ("a-b", "A-b", "a-B", "A-B"):
@@ -291,7 +344,7 @@
 
         # SF bug #432369:
         cf = self.fromstring(
-            "[MySection]\nOption{} first line\n\tsecond line\n".format(
+            "[MySection]\nOption{} first line   \n\tsecond line   \n".format(
                 self.delimiters[0]))
         eq(cf.options("MySection"), ["option"])
         eq(cf.get("MySection", "Option"), "first line\nsecond line")
@@ -303,6 +356,46 @@
         self.assertTrue(cf.has_option("section", "Key"))
 
 
+    def test_case_sensitivity_mapping_access(self):
+        cf = self.newconfig()
+        cf["A"] = {}
+        cf["a"] = {"B": "value"}
+        cf["B"] = {}
+        L = [section for section in cf]
+        L.sort()
+        eq = self.assertEqual
+        elem_eq = self.assertItemsEqual
+        eq(L, ["A", "B", configparser.DEFAULTSECT, "a"])
+        eq(cf["a"].keys(), {"b"})
+        eq(cf["a"]["b"], "value",
+           "could not locate option, expecting case-insensitive option names")
+        with self.assertRaises(KeyError):
+            # section names are case-sensitive
+            cf["b"]["A"] = "value"
+        self.assertTrue("b" in cf["a"])
+        cf["A"]["A-B"] = "A-B value"
+        for opt in ("a-b", "A-b", "a-B", "A-B"):
+            self.assertTrue(
+                opt in cf["A"],
+                "has_option() returned false for option which should exist")
+        eq(cf["A"].keys(), {"a-b"})
+        eq(cf["a"].keys(), {"b"})
+        del cf["a"]["B"]
+        elem_eq(cf["a"].keys(), {})
+
+        # SF bug #432369:
+        cf = self.fromstring(
+            "[MySection]\nOption{} first line   \n\tsecond line   \n".format(
+                self.delimiters[0]))
+        eq(cf["MySection"].keys(), {"option"})
+        eq(cf["MySection"]["Option"], "first line\nsecond line")
+
+        # SF bug #561822:
+        cf = self.fromstring("[section]\n"
+                             "nekey{}nevalue\n".format(self.delimiters[0]),
+                             defaults={"key":"value"})
+        self.assertTrue("Key" in cf["section"])
+
     def test_default_case_sensitivity(self):
         cf = self.newconfig({"foo": "Bar"})
         self.assertEqual(