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 and their keys will be added in order. Values are automatically
converted to strings. 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 Return a string value for the named option. All % interpolations are
expanded in the return values, based on the defaults passed into the expanded in the return values, based on the defaults passed into the
constructor and the DEFAULT section. Additional substitutions may be 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 contents override any pre-existing defaults. If `option' is a key in
`vars', the value from `vars' is used. `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. 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. 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 Like get(), but convert value to a boolean (currently case
insensitively defined as 0, false, no, off for False, and 1, true, insensitively defined as 0, false, no, off for False, and 1, true,
yes, on for True). Returns False or 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. between keys and values are surrounded by spaces.
""" """
try: from collections import MutableMapping, OrderedDict as _default_dict
from collections import OrderedDict as _default_dict import functools
except ImportError:
# fallback for setup.py which hasn't yet built _collections
_default_dict = dict
import io import io
import itertools
import re import re
import sys import sys
import warnings import warnings
@ -366,7 +363,7 @@ _COMPATIBLE = object()
_UNSET = object() _UNSET = object()
class RawConfigParser: class RawConfigParser(MutableMapping):
"""ConfigParser that does not do interpolation.""" """ConfigParser that does not do interpolation."""
# Regular expressions for parsing section headers and options # Regular expressions for parsing section headers and options
@ -413,6 +410,8 @@ class RawConfigParser:
self._dict = dict_type self._dict = dict_type
self._sections = self._dict() self._sections = self._dict()
self._defaults = self._dict() self._defaults = self._dict()
self._views = self._dict()
self._views[DEFAULTSECT] = SectionProxy(self, DEFAULTSECT)
if defaults: if defaults:
for key, value in defaults.items(): for key, value in defaults.items():
self._defaults[self.optionxform(key)] = value self._defaults[self.optionxform(key)] = value
@ -434,6 +433,7 @@ class RawConfigParser:
self._startonly_comment_prefixes = () self._startonly_comment_prefixes = ()
self._comment_prefixes = tuple(comment_prefixes or ()) self._comment_prefixes = tuple(comment_prefixes or ())
self._strict = strict self._strict = strict
self._allow_no_value = allow_no_value
self._empty_lines_in_values = empty_lines_in_values self._empty_lines_in_values = empty_lines_in_values
def defaults(self): def defaults(self):
@ -451,12 +451,13 @@ class RawConfigParser:
already exists. Raise ValueError if name is DEFAULT or any of it's already exists. Raise ValueError if name is DEFAULT or any of it's
case-insensitive variants. case-insensitive variants.
""" """
if section.lower() == "default": if section.upper() == DEFAULTSECT:
raise ValueError('Invalid section name: %s' % section) raise ValueError('Invalid section name: %s' % section)
if section in self._sections: if section in self._sections:
raise DuplicateSectionError(section) raise DuplicateSectionError(section)
self._sections[section] = self._dict() self._sections[section] = self._dict()
self._views[section] = SectionProxy(self, section)
def has_section(self, section): def has_section(self, section):
"""Indicate whether the named section is present in the configuration. """Indicate whether the named section is present in the configuration.
@ -534,7 +535,7 @@ class RawConfigParser:
for section, keys in dictionary.items(): for section, keys in dictionary.items():
try: try:
self.add_section(section) self.add_section(section)
except DuplicateSectionError: except (DuplicateSectionError, ValueError):
if self._strict and section in elements_added: if self._strict and section in elements_added:
raise raise
elements_added.add(section) elements_added.add(section)
@ -556,29 +557,31 @@ class RawConfigParser:
) )
self.read_file(fp, source=filename) 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. """Get an option value for a given section.
If `vars' is provided, it must be a dictionary. The option is looked up 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. 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 If the key is not found and `fallback' is provided, it is used as
a fallback value. `None' can be provided as a `default' value. a fallback value. `None' can be provided as a `fallback' value.
Arguments `vars' and `fallback' are keyword only.
""" """
try: try:
d = self._unify_values(section, vars) d = self._unify_values(section, vars)
except NoSectionError: except NoSectionError:
if default is _UNSET: if fallback is _UNSET:
raise raise
else: else:
return default return fallback
option = self.optionxform(option) option = self.optionxform(option)
try: try:
return d[option] return d[option]
except KeyError: except KeyError:
if default is _UNSET: if fallback is _UNSET:
raise NoOptionError(option, section) raise NoOptionError(option, section)
else: else:
return default return fallback
def items(self, section): def items(self, section):
try: try:
@ -593,35 +596,36 @@ class RawConfigParser:
del d["__name__"] del d["__name__"]
return d.items() return d.items()
def _get(self, section, conv, option, *args, **kwargs): def _get(self, section, conv, option, **kwargs):
return conv(self.get(section, option, *args, **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: try:
return self._get(section, int, option, vars) return self._get(section, int, option, vars=vars)
except (NoSectionError, NoOptionError): except (NoSectionError, NoOptionError):
if default is _UNSET: if fallback is _UNSET:
raise raise
else: else:
return default return fallback
def getfloat(self, section, option, vars=None, default=_UNSET): def getfloat(self, section, option, *, vars=None, fallback=_UNSET):
try: try:
return self._get(section, float, option, vars) return self._get(section, float, option, vars=vars)
except (NoSectionError, NoOptionError): except (NoSectionError, NoOptionError):
if default is _UNSET: if fallback is _UNSET:
raise raise
else: else:
return default return fallback
def getboolean(self, section, option, vars=None, default=_UNSET): def getboolean(self, section, option, *, vars=None, fallback=_UNSET):
try: 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): except (NoSectionError, NoOptionError):
if default is _UNSET: if fallback is _UNSET:
raise raise
else: else:
return default return fallback
def optionxform(self, optionstr): def optionxform(self, optionstr):
return optionstr.lower() return optionstr.lower()
@ -671,7 +675,7 @@ class RawConfigParser:
for key, value in section_items: for key, value in section_items:
if key == "__name__": if key == "__name__":
continue 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') value = delimiter + str(value).replace('\n', '\n\t')
else: else:
value = "" value = ""
@ -698,8 +702,40 @@ class RawConfigParser:
existed = section in self._sections existed = section in self._sections
if existed: if existed:
del self._sections[section] del self._sections[section]
del self._views[section]
return existed 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): def _read(self, fp, fpname):
"""Parse a sectioned configuration file. """Parse a sectioned configuration file.
@ -776,6 +812,7 @@ class RawConfigParser:
cursect = self._dict() cursect = self._dict()
cursect['__name__'] = sectname cursect['__name__'] = sectname
self._sections[sectname] = cursect self._sections[sectname] = cursect
self._views[sectname] = SectionProxy(self, sectname)
elements_added.add(sectname) elements_added.add(sectname)
# So sections can't start with a continuation line # So sections can't start with a continuation line
optname = None optname = None
@ -818,8 +855,8 @@ class RawConfigParser:
self._join_multiline_values() self._join_multiline_values()
def _join_multiline_values(self): def _join_multiline_values(self):
all_sections = [self._defaults] all_sections = itertools.chain((self._defaults,),
all_sections.extend(self._sections.values()) self._sections.values())
for options in all_sections: for options in all_sections:
for name, val in options.items(): for name, val in options.items():
if isinstance(val, list): if isinstance(val, list):
@ -857,73 +894,95 @@ class RawConfigParser:
raise ValueError('Not a boolean: %s' % value) raise ValueError('Not a boolean: %s' % value)
return self.BOOLEAN_STATES[value.lower()] 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): class ConfigParser(RawConfigParser):
"""ConfigParser implementing interpolation.""" """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. """Get an option value for a given section.
If `vars' is provided, it must be a dictionary. The option is looked up 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. 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 If the key is not found and `fallback' is provided, it is used as
a fallback value. `None' can be provided as a `default' value. a fallback value. `None' can be provided as a `fallback' value.
All % interpolations are expanded in the return values, unless the All % interpolations are expanded in the return values, unless the
optional argument `raw' is true. Values for interpolation keys are optional argument `raw' is true. Values for interpolation keys are
looked up in the same manner as the option. looked up in the same manner as the option.
Arguments `raw', `vars', and `fallback' are keyword only.
The section DEFAULT is special. The section DEFAULT is special.
""" """
try: try:
d = self._unify_values(section, vars) d = self._unify_values(section, vars)
except NoSectionError: except NoSectionError:
if default is _UNSET: if fallback is _UNSET:
raise raise
else: else:
return default return fallback
option = self.optionxform(option) option = self.optionxform(option)
try: try:
value = d[option] value = d[option]
except KeyError: except KeyError:
if default is _UNSET: if fallback is _UNSET:
raise NoOptionError(option, section) raise NoOptionError(option, section)
else: else:
return default return fallback
if raw or value is None: if raw or value is None:
return value return value
else: else:
return self._interpolate(section, option, value, d) 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: try:
return self._get(section, int, option, raw, vars) return self._get(section, int, option, raw=raw, vars=vars)
except (NoSectionError, NoOptionError): except (NoSectionError, NoOptionError):
if default is _UNSET: if fallback is _UNSET:
raise raise
else: 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: try:
return self._get(section, float, option, raw, vars) return self._get(section, float, option, raw=raw, vars=vars)
except (NoSectionError, NoOptionError): except (NoSectionError, NoOptionError):
if default is _UNSET: if fallback is _UNSET:
raise raise
else: else:
return default return fallback
def getboolean(self, section, option, raw=False, vars=None, def getboolean(self, section, option, *, raw=False, vars=None,
default=_UNSET): fallback=_UNSET):
try: try:
return self._get(section, self._convert_to_boolean, option, raw, return self._get(section, self._convert_to_boolean, option,
vars) raw=raw, vars=vars)
except (NoSectionError, NoOptionError): except (NoSectionError, NoOptionError):
if default is _UNSET: if fallback is _UNSET:
raise raise
else: else:
return default return fallback
def items(self, section, raw=False, vars=None): def items(self, section, raw=False, vars=None):
"""Return a list of (name, value) tuples for each option in a section. """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): def set(self, section, option, value=None):
"""Set an option. Extend ConfigParser.set: check for string values.""" """Set an option. Extend ConfigParser.set: check for string values."""
# The only legal non-string value if we allow valueless self._validate_value_type(value)
# 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")
# check for bad percent signs # check for bad percent signs
if value: if value:
tmp_value = value.replace('%%', '') # escaped percent signs tmp_value = value.replace('%%', '') # escaped percent signs
@ -1053,3 +1105,60 @@ class SafeConfigParser(ConfigParser):
raise ValueError("invalid interpolation syntax in %r at " raise ValueError("invalid interpolation syntax in %r at "
"position %d" % (value, tmp_value.find('%'))) "position %d" % (value, tmp_value.find('%')))
ConfigParser.set(self, section, option, value) 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): def _create_formatters(cp):
"""Create and return formatters""" """Create and return formatters"""
flist = cp.get("formatters", "keys") flist = cp["formatters"]["keys"]
if not len(flist): if not len(flist):
return {} return {}
flist = flist.split(",") flist = flist.split(",")
@ -111,20 +111,12 @@ def _create_formatters(cp):
formatters = {} formatters = {}
for form in flist: for form in flist:
sectname = "formatter_%s" % form sectname = "formatter_%s" % form
opts = cp.options(sectname) fs = cp.get(sectname, "format", raw=True, fallback=None)
if "format" in opts: dfs = cp.get(sectname, "datefmt", raw=True, fallback=None)
fs = cp.get(sectname, "format", 1)
else:
fs = None
if "datefmt" in opts:
dfs = cp.get(sectname, "datefmt", 1)
else:
dfs = None
c = logging.Formatter c = logging.Formatter
if "class" in opts: class_name = cp[sectname].get("class")
class_name = cp.get(sectname, "class") if class_name:
if class_name: c = _resolve(class_name)
c = _resolve(class_name)
f = c(fs, dfs) f = c(fs, dfs)
formatters[form] = f formatters[form] = f
return formatters return formatters
@ -132,7 +124,7 @@ def _create_formatters(cp):
def _install_handlers(cp, formatters): def _install_handlers(cp, formatters):
"""Install and return handlers""" """Install and return handlers"""
hlist = cp.get("handlers", "keys") hlist = cp["handlers"]["keys"]
if not len(hlist): if not len(hlist):
return {} return {}
hlist = hlist.split(",") hlist = hlist.split(",")
@ -140,30 +132,23 @@ def _install_handlers(cp, formatters):
handlers = {} handlers = {}
fixups = [] #for inter-handler references fixups = [] #for inter-handler references
for hand in hlist: for hand in hlist:
sectname = "handler_%s" % hand section = cp["handler_%s" % hand]
klass = cp.get(sectname, "class") klass = section["class"]
opts = cp.options(sectname) fmt = section.get("formatter", "")
if "formatter" in opts:
fmt = cp.get(sectname, "formatter")
else:
fmt = ""
try: try:
klass = eval(klass, vars(logging)) klass = eval(klass, vars(logging))
except (AttributeError, NameError): except (AttributeError, NameError):
klass = _resolve(klass) klass = _resolve(klass)
args = cp.get(sectname, "args") args = section["args"]
args = eval(args, vars(logging)) args = eval(args, vars(logging))
h = klass(*args) h = klass(*args)
if "level" in opts: if "level" in section:
level = cp.get(sectname, "level") level = section["level"]
h.setLevel(logging._levelNames[level]) h.setLevel(logging._levelNames[level])
if len(fmt): if len(fmt):
h.setFormatter(formatters[fmt]) h.setFormatter(formatters[fmt])
if issubclass(klass, logging.handlers.MemoryHandler): if issubclass(klass, logging.handlers.MemoryHandler):
if "target" in opts: target = section.get("target", "")
target = cp.get(sectname,"target")
else:
target = ""
if len(target): #the target handler may not be loaded yet, so keep for later... if len(target): #the target handler may not be loaded yet, so keep for later...
fixups.append((h, target)) fixups.append((h, target))
handlers[hand] = h handlers[hand] = h
@ -197,20 +182,19 @@ def _install_loggers(cp, handlers, disable_existing):
"""Create and install loggers""" """Create and install loggers"""
# configure the root first # configure the root first
llist = cp.get("loggers", "keys") llist = cp["loggers"]["keys"]
llist = llist.split(",") llist = llist.split(",")
llist = list(map(lambda x: x.strip(), llist)) llist = list(map(lambda x: x.strip(), llist))
llist.remove("root") llist.remove("root")
sectname = "logger_root" section = cp["logger_root"]
root = logging.root root = logging.root
log = root log = root
opts = cp.options(sectname) if "level" in section:
if "level" in opts: level = section["level"]
level = cp.get(sectname, "level")
log.setLevel(logging._levelNames[level]) log.setLevel(logging._levelNames[level])
for h in root.handlers[:]: for h in root.handlers[:]:
root.removeHandler(h) root.removeHandler(h)
hlist = cp.get(sectname, "handlers") hlist = section["handlers"]
if len(hlist): if len(hlist):
hlist = hlist.split(",") hlist = hlist.split(",")
hlist = _strip_spaces(hlist) hlist = _strip_spaces(hlist)
@ -237,13 +221,9 @@ def _install_loggers(cp, handlers, disable_existing):
child_loggers = [] child_loggers = []
#now set up the new ones... #now set up the new ones...
for log in llist: for log in llist:
sectname = "logger_%s" % log section = cp["logger_%s" % log]
qn = cp.get(sectname, "qualname") qn = section["qualname"]
opts = cp.options(sectname) propagate = section.getint("propagate", fallback=1)
if "propagate" in opts:
propagate = cp.getint(sectname, "propagate")
else:
propagate = 1
logger = logging.getLogger(qn) logger = logging.getLogger(qn)
if qn in existing: if qn in existing:
i = existing.index(qn) i = existing.index(qn)
@ -255,14 +235,14 @@ def _install_loggers(cp, handlers, disable_existing):
child_loggers.append(existing[i]) child_loggers.append(existing[i])
i = i + 1 i = i + 1
existing.remove(qn) existing.remove(qn)
if "level" in opts: if "level" in section:
level = cp.get(sectname, "level") level = section["level"]
logger.setLevel(logging._levelNames[level]) logger.setLevel(logging._levelNames[level])
for h in logger.handlers[:]: for h in logger.handlers[:]:
logger.removeHandler(h) logger.removeHandler(h)
logger.propagate = propagate logger.propagate = propagate
logger.disabled = 0 logger.disabled = 0
hlist = cp.get(sectname, "handlers") hlist = section["handlers"]
if len(hlist): if len(hlist):
hlist = hlist.split(",") hlist = hlist.split(",")
hlist = _strip_spaces(hlist) hlist = _strip_spaces(hlist)

View File

@ -52,8 +52,6 @@ class CfgParserTestCaseClass(unittest.TestCase):
class BasicTestCase(CfgParserTestCaseClass): class BasicTestCase(CfgParserTestCaseClass):
def basic_test(self, cf): def basic_test(self, cf):
L = cf.sections()
L.sort()
E = ['Commented Bar', E = ['Commented Bar',
'Foo Bar', 'Foo Bar',
'Internationalized Stuff', 'Internationalized Stuff',
@ -64,20 +62,34 @@ class BasicTestCase(CfgParserTestCaseClass):
'Spacey Bar From The Beginning', 'Spacey Bar From The Beginning',
'Types', 'Types',
] ]
if self.allow_no_value: if self.allow_no_value:
E.append('NoValue') E.append('NoValue')
E.sort() E.sort()
# API access
L = cf.sections()
L.sort()
eq = self.assertEqual eq = self.assertEqual
eq(L, E) 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 # The use of spaces in the section names serves as a
# regression test for SourceForge bug #583248: # regression test for SourceForge bug #583248:
# http://www.python.org/sf/583248 # http://www.python.org/sf/583248
eq(cf.get('Foo Bar', 'foo'), 'bar')
eq(cf.get('Spacey Bar', 'foo'), 'bar') # API access
eq(cf.get('Spacey Bar From The Beginning', 'foo'), 'bar') 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('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('Commented Bar', 'baz'), 'qwe')
eq(cf.get('Spaces', 'key with spaces'), 'value') eq(cf.get('Spaces', 'key with spaces'), 'value')
eq(cf.get('Spaces', 'another with spaces'), 'splat!') eq(cf.get('Spaces', 'another with spaces'), 'splat!')
@ -89,40 +101,69 @@ class BasicTestCase(CfgParserTestCaseClass):
if self.allow_no_value: if self.allow_no_value:
eq(cf.get('NoValue', 'option-without-value'), None) eq(cf.get('NoValue', 'option-without-value'), None)
# test vars= and default= # test vars= and fallback=
eq(cf.get('Foo Bar', 'foo', default='baz'), 'bar') eq(cf.get('Foo Bar', 'foo', fallback='baz'), 'bar1')
eq(cf.get('Foo Bar', 'foo', vars={'foo': 'baz'}), 'baz') eq(cf.get('Foo Bar', 'foo', vars={'foo': 'baz'}), 'baz')
with self.assertRaises(configparser.NoSectionError): with self.assertRaises(configparser.NoSectionError):
cf.get('No Such Foo Bar', 'foo') cf.get('No Such Foo Bar', 'foo')
with self.assertRaises(configparser.NoOptionError): with self.assertRaises(configparser.NoOptionError):
cf.get('Foo Bar', 'no-such-foo') cf.get('Foo Bar', 'no-such-foo')
eq(cf.get('No Such Foo Bar', 'foo', default='baz'), 'baz') eq(cf.get('No Such Foo Bar', 'foo', fallback='baz'), 'baz')
eq(cf.get('Foo Bar', 'no-such-foo', default='baz'), 'baz') eq(cf.get('Foo Bar', 'no-such-foo', fallback='baz'), 'baz')
eq(cf.get('Spacey Bar', 'foo', default=None), 'bar') eq(cf.get('Spacey Bar', 'foo', fallback=None), 'bar2')
eq(cf.get('No Such Spacey Bar', 'foo', default=None), None) eq(cf.get('No Such Spacey Bar', 'foo', fallback=None), None)
eq(cf.getint('Types', 'int', default=18), 42) eq(cf.getint('Types', 'int', fallback=18), 42)
eq(cf.getint('Types', 'no-such-int', default=18), 18) eq(cf.getint('Types', 'no-such-int', fallback=18), 18)
eq(cf.getint('Types', 'no-such-int', default="18"), "18") # sic! eq(cf.getint('Types', 'no-such-int', fallback="18"), "18") # sic!
self.assertAlmostEqual(cf.getfloat('Types', 'float', self.assertAlmostEqual(cf.getfloat('Types', 'float',
default=0.0), 0.44) fallback=0.0), 0.44)
self.assertAlmostEqual(cf.getfloat('Types', 'no-such-float', self.assertAlmostEqual(cf.getfloat('Types', 'no-such-float',
default=0.0), 0.0) fallback=0.0), 0.0)
eq(cf.getfloat('Types', 'no-such-float', default="0.0"), "0.0") # sic! eq(cf.getfloat('Types', 'no-such-float', fallback="0.0"), "0.0") # sic!
eq(cf.getboolean('Types', 'boolean', default=True), False) eq(cf.getboolean('Types', 'boolean', fallback=True), False)
eq(cf.getboolean('Types', 'no-such-boolean', default="yes"), eq(cf.getboolean('Types', 'no-such-boolean', fallback="yes"),
"yes") # sic! "yes") # sic!
eq(cf.getboolean('Types', 'no-such-boolean', default=True), True) eq(cf.getboolean('Types', 'no-such-boolean', fallback=True), True)
eq(cf.getboolean('No Such Types', 'boolean', default=True), True) eq(cf.getboolean('No Such Types', 'boolean', fallback=True), True)
if self.allow_no_value: 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', 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"), self.assertNotIn('__name__', cf.options("Foo Bar"),
'__name__ "option" should not be exposed by the API!') '__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(); # Make sure the right things happen for remove_option();
# added to include check for SourceForge bug #123324: # added to include check for SourceForge bug #123324:
# API acceess
self.assertTrue(cf.remove_option('Foo Bar', 'foo'), self.assertTrue(cf.remove_option('Foo Bar', 'foo'),
"remove_option() failed to report existence of option") "remove_option() failed to report existence of option")
self.assertFalse(cf.has_option('Foo Bar', 'foo'), self.assertFalse(cf.has_option('Foo Bar', 'foo'),
@ -138,17 +179,25 @@ class BasicTestCase(CfgParserTestCaseClass):
eq(cf.get('Long Line', 'foo'), eq(cf.get('Long Line', 'foo'),
'this line is much, much longer than my editor\nlikes it.') '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): def test_basic(self):
config_string = """\ config_string = """\
[Foo Bar] [Foo Bar]
foo{0[0]}bar foo{0[0]}bar1
[Spacey Bar] [Spacey Bar]
foo {0[0]} bar foo {0[0]} bar2
[Spacey Bar From The Beginning] [Spacey Bar From The Beginning]
foo {0[0]} bar foo {0[0]} bar3
baz {0[0]} qwe baz {0[0]} qwe
[Commented Bar] [Commented Bar]
foo{0[1]} bar {1[1]} comment foo{0[1]} bar4 {1[1]} comment
baz{0[0]}qwe {1[0]}another one baz{0[0]}qwe {1[0]}another one
[Long Line] [Long Line]
foo{0[1]} this line is much, much longer than my editor 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): def test_basic_from_dict(self):
config = { config = {
"Foo Bar": { "Foo Bar": {
"foo": "bar", "foo": "bar1",
}, },
"Spacey Bar": { "Spacey Bar": {
"foo": "bar", "foo": "bar2",
}, },
"Spacey Bar From The Beginning": { "Spacey Bar From The Beginning": {
"foo": "bar", "foo": "bar3",
"baz": "qwe", "baz": "qwe",
}, },
"Commented Bar": { "Commented Bar": {
"foo": "bar", "foo": "bar4",
"baz": "qwe", "baz": "qwe",
}, },
"Long Line": { "Long Line": {
@ -270,14 +319,18 @@ boolean {0[0]} NO
cf = self.newconfig() cf = self.newconfig()
cf.add_section("A") cf.add_section("A")
cf.add_section("a") cf.add_section("a")
cf.add_section("B")
L = cf.sections() L = cf.sections()
L.sort() L.sort()
eq = self.assertEqual eq = self.assertEqual
eq(L, ["A", "a"]) eq(L, ["A", "B", "a"])
cf.set("a", "B", "value") cf.set("a", "B", "value")
eq(cf.options("a"), ["b"]) eq(cf.options("a"), ["b"])
eq(cf.get("a", "b"), "value", eq(cf.get("a", "b"), "value",
"could not locate option, expecting case-insensitive option names") "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")) self.assertTrue(cf.has_option("a", "b"))
cf.set("A", "A-B", "A-B value") cf.set("A", "A-B", "A-B value")
for opt in ("a-b", "A-b", "a-B", "A-B"): for opt in ("a-b", "A-b", "a-B", "A-B"):
@ -291,7 +344,7 @@ boolean {0[0]} NO
# SF bug #432369: # SF bug #432369:
cf = self.fromstring( cf = self.fromstring(
"[MySection]\nOption{} first line\n\tsecond line\n".format( "[MySection]\nOption{} first line \n\tsecond line \n".format(
self.delimiters[0])) self.delimiters[0]))
eq(cf.options("MySection"), ["option"]) eq(cf.options("MySection"), ["option"])
eq(cf.get("MySection", "Option"), "first line\nsecond line") eq(cf.get("MySection", "Option"), "first line\nsecond line")
@ -303,6 +356,46 @@ boolean {0[0]} NO
self.assertTrue(cf.has_option("section", "Key")) 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): def test_default_case_sensitivity(self):
cf = self.newconfig({"foo": "Bar"}) cf = self.newconfig({"foo": "Bar"})
self.assertEqual( self.assertEqual(