- apply patch from issue 7005

- add corresponding documentation
This commit is contained in:
Fred Drake 2010-02-19 05:24:30 +00:00
parent c2294dd6ba
commit cc43b56960
3 changed files with 147 additions and 42 deletions

View File

@ -62,12 +62,16 @@ dictionary type is passed that sorts its keys, the sections will be sorted on
write-back, as will be the keys within each section.
.. class:: RawConfigParser([defaults[, dict_type]])
.. class:: RawConfigParser([defaults[, dict_type[, allow_no_value]]])
The basic configuration object. When *defaults* is given, it is initialized
into the dictionary of intrinsic defaults. When *dict_type* is given, it will
be used to create the dictionary objects for the list of sections, for the
options within a section, and for the default values. This class does not
options within a section, and for the default values. When *allow_no_value*
is true (default: ``False``), options without values are accepted; the value
presented for these is ``None``.
This class does not
support the magical interpolation behavior.
.. versionadded:: 2.3
@ -77,9 +81,10 @@ write-back, as will be the keys within each section.
.. versionchanged:: 2.7
The default *dict_type* is :class:`collections.OrderedDict`.
*allow_no_value* was added.
.. class:: ConfigParser([defaults[, dict_type]])
.. class:: ConfigParser([defaults[, dict_type[, allow_no_value]]])
Derived class of :class:`RawConfigParser` that implements the magical
interpolation feature and adds optional arguments to the :meth:`get` and
@ -101,9 +106,10 @@ write-back, as will be the keys within each section.
.. versionchanged:: 2.7
The default *dict_type* is :class:`collections.OrderedDict`.
*allow_no_value* was added.
.. class:: SafeConfigParser([defaults[, dict_type]])
.. class:: SafeConfigParser([defaults[, dict_type[, allow_no_value]]])
Derived class of :class:`ConfigParser` that implements a more-sane variant of
the magical interpolation feature. This implementation is more predictable as
@ -119,6 +125,7 @@ write-back, as will be the keys within each section.
.. versionchanged:: 2.7
The default *dict_type* is :class:`collections.OrderedDict`.
*allow_no_value* was added.
.. exception:: NoSectionError
@ -484,3 +491,38 @@ The function ``opt_move`` below can be used to move options between sections::
opt_move(config, section1, section2, option)
else:
config.remove_option(section1, option)
Some configuration files are known to include settings without values, but which
otherwise conform to the syntax supported by :mod:`ConfigParser`. The
*allow_no_value* parameter to the constructor can be used to indicate that such
values should be accepted:
.. doctest::
>>> import ConfigParser
>>> import io
>>> sample_config = """
... [mysqld]
... user = mysql
... pid-file = /var/run/mysqld/mysqld.pid
... skip-external-locking
... old_passwords = 1
... skip-bdb
... skip-innodb
... """
>>> config = ConfigParser.RawConfigParser(allow_no_value=True)
>>> config.readfp(io.BytesIO(sample_config))
>>> # Settings with values are treated as before:
>>> config.get("mysqld", "user")
'mysql'
>>> # Settings without values provide None:
>>> config.get("mysqld", "skip-bdb")
>>> # Settings which aren't specified still raise an error:
>>> config.get("mysqld", "does-not-exist")
Traceback (most recent call last):
...
ConfigParser.NoOptionError: No option 'does-not-exist' in section: 'mysqld'

View File

@ -221,10 +221,15 @@ class MissingSectionHeaderError(ParsingError):
class RawConfigParser:
def __init__(self, defaults=None, dict_type=_default_dict):
def __init__(self, defaults=None, dict_type=_default_dict,
allow_no_value=False):
self._dict = dict_type
self._sections = self._dict()
self._defaults = self._dict()
if allow_no_value:
self._optcre = self.OPTCRE_NV
else:
self._optcre = self.OPTCRE
if defaults:
for key, value in defaults.items():
self._defaults[self.optionxform(key)] = value
@ -372,7 +377,7 @@ class RawConfigParser:
return (option in self._sections[section]
or option in self._defaults)
def set(self, section, option, value):
def set(self, section, option, value=None):
"""Set an option."""
if not section or section == DEFAULTSECT:
sectdict = self._defaults
@ -394,8 +399,11 @@ class RawConfigParser:
fp.write("[%s]\n" % section)
for (key, value) in self._sections[section].items():
if key != "__name__":
fp.write("%s = %s\n" %
(key, str(value).replace('\n', '\n\t')))
if value is None:
fp.write("%s\n" % (key))
else:
fp.write("%s = %s\n" %
(key, str(value).replace('\n', '\n\t')))
fp.write("\n")
def remove_option(self, section, option):
@ -436,6 +444,15 @@ class RawConfigParser:
# by any # space/tab
r'(?P<value>.*)$' # everything up to eol
)
OPTCRE_NV = re.compile(
r'(?P<option>[^:=\s][^:=]*)' # very permissive!
r'\s*(?:' # any number of space/tab,
r'(?P<vi>[:=])\s*' # optionally followed by
# separator (either : or
# =), followed by any #
# space/tab
r'(?P<value>.*))?$' # everything up to eol
)
def _read(self, fp, fpname):
"""Parse a sectioned setup file.
@ -488,16 +505,19 @@ class RawConfigParser:
raise MissingSectionHeaderError(fpname, lineno, line)
# an option line?
else:
mo = self.OPTCRE.match(line)
mo = self._optcre.match(line)
if mo:
optname, vi, optval = mo.group('option', 'vi', 'value')
if vi in ('=', ':') and ';' in optval:
# ';' is a comment delimiter only if it follows
# a spacing character
pos = optval.find(';')
if pos != -1 and optval[pos-1].isspace():
optval = optval[:pos]
optval = optval.strip()
# This check is fine because the OPTCRE cannot
# match if it would set optval to None
if optval is not None:
if vi in ('=', ':') and ';' in optval:
# ';' is a comment delimiter only if it follows
# a spacing character
pos = optval.find(';')
if pos != -1 and optval[pos-1].isspace():
optval = optval[:pos]
optval = optval.strip()
# allow empty values
if optval == '""':
optval = ''
@ -545,7 +565,7 @@ class ConfigParser(RawConfigParser):
except KeyError:
raise NoOptionError(option, section)
if raw:
if raw or value is None:
return value
else:
return self._interpolate(section, option, value, d)
@ -588,7 +608,7 @@ class ConfigParser(RawConfigParser):
depth = MAX_INTERPOLATION_DEPTH
while depth: # Loop through this until it's done
depth -= 1
if "%(" in value:
if value and "%(" in value:
value = self._KEYCRE.sub(self._interpolation_replace, value)
try:
value = value % vars
@ -597,7 +617,7 @@ class ConfigParser(RawConfigParser):
option, section, rawval, e.args[0])
else:
break
if "%(" in value:
if value and "%(" in value:
raise InterpolationDepthError(option, section, rawval)
return value
@ -659,10 +679,16 @@ class SafeConfigParser(ConfigParser):
option, section,
"'%%' must be followed by '%%' or '(', found: %r" % (rest,))
def set(self, section, option, value):
def set(self, section, option, value=None):
"""Set an option. Extend ConfigParser.set: check for string values."""
if not isinstance(value, basestring):
raise TypeError("option values must be strings")
# 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, basestring):
raise TypeError("option values must be strings")
# check for bad percent signs:
# first, replace all "good" interpolations
tmp_value = value.replace('%%', '')

View File

@ -5,6 +5,7 @@ import UserDict
from test import test_support
class SortedDict(UserDict.UserDict):
def items(self):
result = self.data.items()
@ -26,12 +27,16 @@ class SortedDict(UserDict.UserDict):
__iter__ = iterkeys
def itervalues(self): return iter(self.values())
class TestCaseBase(unittest.TestCase):
allow_no_value = False
def newconfig(self, defaults=None):
if defaults is None:
self.cf = self.config_class()
self.cf = self.config_class(allow_no_value=self.allow_no_value)
else:
self.cf = self.config_class(defaults)
self.cf = self.config_class(defaults,
allow_no_value=self.allow_no_value)
return self.cf
def fromstring(self, string, defaults=None):
@ -41,7 +46,7 @@ class TestCaseBase(unittest.TestCase):
return cf
def test_basic(self):
cf = self.fromstring(
config_string = (
"[Foo Bar]\n"
"foo=bar\n"
"[Spacey Bar]\n"
@ -61,17 +66,28 @@ class TestCaseBase(unittest.TestCase):
"key with spaces : value\n"
"another with spaces = splat!\n"
)
if self.allow_no_value:
config_string += (
"[NoValue]\n"
"option-without-value\n"
)
cf = self.fromstring(config_string)
L = cf.sections()
L.sort()
E = [r'Commented Bar',
r'Foo Bar',
r'Internationalized Stuff',
r'Long Line',
r'Section\with$weird%characters[' '\t',
r'Spaces',
r'Spacey Bar',
]
if self.allow_no_value:
E.append(r'NoValue')
E.sort()
eq = self.assertEqual
eq(L, [r'Commented Bar',
r'Foo Bar',
r'Internationalized Stuff',
r'Long Line',
r'Section\with$weird%characters[' '\t',
r'Spaces',
r'Spacey Bar',
])
eq(L, E)
# The use of spaces in the section names serves as a
# regression test for SourceForge bug #583248:
@ -81,6 +97,8 @@ class TestCaseBase(unittest.TestCase):
eq(cf.get('Commented Bar', 'foo'), 'bar')
eq(cf.get('Spaces', 'key with spaces'), 'value')
eq(cf.get('Spaces', 'another with spaces'), 'splat!')
if self.allow_no_value:
eq(cf.get('NoValue', 'option-without-value'), None)
self.assertNotIn('__name__', cf.options("Foo Bar"),
'__name__ "option" should not be exposed by the API!')
@ -152,8 +170,6 @@ class TestCaseBase(unittest.TestCase):
"[Foo]\n extra-spaces: splat\n")
self.parse_error(ConfigParser.ParsingError,
"[Foo]\n extra-spaces= splat\n")
self.parse_error(ConfigParser.ParsingError,
"[Foo]\noption-without-value\n")
self.parse_error(ConfigParser.ParsingError,
"[Foo]\n:value-without-option-name\n")
self.parse_error(ConfigParser.ParsingError,
@ -220,18 +236,24 @@ class TestCaseBase(unittest.TestCase):
cf.add_section, "Foo")
def test_write(self):
cf = self.fromstring(
config_string = (
"[Long Line]\n"
"foo: this line is much, much longer than my editor\n"
" likes it.\n"
"[DEFAULT]\n"
"foo: another very\n"
" long line"
" long line\n"
)
if self.allow_no_value:
config_string += (
"[Valueless]\n"
"option-without-value\n"
)
cf = self.fromstring(config_string)
output = StringIO.StringIO()
cf.write(output)
self.assertEqual(
output.getvalue(),
expect_string = (
"[DEFAULT]\n"
"foo = another very\n"
"\tlong line\n"
@ -241,6 +263,13 @@ class TestCaseBase(unittest.TestCase):
"\tlikes it.\n"
"\n"
)
if self.allow_no_value:
expect_string += (
"[Valueless]\n"
"option-without-value\n"
"\n"
)
self.assertEqual(output.getvalue(), expect_string)
def test_set_string_types(self):
cf = self.fromstring("[sect]\n"
@ -339,7 +368,7 @@ class ConfigParserTestCase(TestCaseBase):
self.get_error(ConfigParser.InterpolationDepthError, "Foo", "bar11")
def test_interpolation_missing_value(self):
cf = self.get_interpolation_config()
self.get_interpolation_config()
e = self.get_error(ConfigParser.InterpolationError,
"Interpolation Error", "name")
self.assertEqual(e.reference, "reference")
@ -459,6 +488,11 @@ class SafeConfigParserTestCase(ConfigParserTestCase):
cf = self.newconfig()
self.assertRaises(ValueError, cf.add_section, "DEFAULT")
class SafeConfigParserTestCaseNoValue(SafeConfigParserTestCase):
allow_no_value = True
class SortedTestCase(RawConfigParserTestCase):
def newconfig(self, defaults=None):
self.cf = self.config_class(defaults=defaults, dict_type=SortedDict)
@ -483,13 +517,16 @@ class SortedTestCase(RawConfigParserTestCase):
"o3 = 2\n"
"o4 = 1\n\n")
def test_main():
test_support.run_unittest(
ConfigParserTestCase,
RawConfigParserTestCase,
SafeConfigParserTestCase,
SortedTestCase
)
SortedTestCase,
SafeConfigParserTestCaseNoValue,
)
if __name__ == "__main__":
test_main()