Issue #5412: extend configparser to support mapping access

This commit is contained in:
Łukasz Langa 2010-11-10 18:57:39 +00:00
parent 47f637ce17
commit 26d513cf2f
4 changed files with 1086 additions and 433 deletions

File diff suppressed because it is too large Load Diff

View File

@ -85,7 +85,7 @@ ConfigParser -- responsible for parsing a list of
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 @@ ConfigParser -- responsible for parsing a list of
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 @@ ConfigParser -- responsible for parsing a list of
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 @@ _COMPATIBLE = object()
_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 @@ class RawConfigParser:
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 @@ class RawConfigParser:
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 @@ class RawConfigParser:
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 @@ class RawConfigParser:
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 @@ class RawConfigParser:
)
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 @@ class RawConfigParser:
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 @@ class RawConfigParser:
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 @@ class RawConfigParser:
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 @@ class RawConfigParser:
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 @@ class RawConfigParser:
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 @@ class RawConfigParser:
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 @@ class SafeConfigParser(ConfigParser):
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 @@ class SafeConfigParser(ConfigParser):
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__()

View File

@ -103,7 +103,7 @@ def _encoded(s):
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 @@ def _create_formatters(cp):
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 _create_formatters(cp):
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 @@ def _install_handlers(cp, formatters):
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 @@ def _install_loggers(cp, handlers, disable_existing):
"""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 @@ def _install_loggers(cp, handlers, disable_existing):
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 @@ def _install_loggers(cp, handlers, disable_existing):
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)

View File

@ -52,8 +52,6 @@ class CfgParserTestCaseClass(unittest.TestCase):
class BasicTestCase(CfgParserTestCaseClass):
def basic_test(self, cf):
L = cf.sections()
L.sort()
E = ['Commented Bar',
'Foo Bar',
'Internationalized Stuff',
@ -64,20 +62,34 @@ class BasicTestCase(CfgParserTestCaseClass):
'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 @@ class BasicTestCase(CfgParserTestCaseClass):
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 @@ class BasicTestCase(CfgParserTestCaseClass):
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 @@ boolean {0[0]} NO
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 @@ boolean {0[0]} NO
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 @@ boolean {0[0]} NO
# 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 @@ boolean {0[0]} NO
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(