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

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__()