#1682942: add some ConfigParser features: alternate delimiters, alternate comments, empty lines in values. Also enhance the docs with more examples and mention SafeConfigParser before ConfigParser. Patch by Lukas Langa, review by myself, Eric and Ezio.

This commit is contained in:
Georg Brandl 2010-07-28 13:13:46 +00:00
parent cbb0ae4a42
commit 96a60ae90c
4 changed files with 546 additions and 281 deletions

View file

@ -3,6 +3,7 @@ import configparser
import io
import os
import unittest
import textwrap
from test import support
@ -23,15 +24,26 @@ class SortedDict(collections.UserDict):
def itervalues(self): return iter(self.values())
class TestCaseBase(unittest.TestCase):
class CfgParserTestCaseClass(unittest.TestCase):
allow_no_value = False
delimiters = ('=', ':')
comment_prefixes = (';', '#')
empty_lines_in_values = True
dict_type = configparser._default_dict
def newconfig(self, defaults=None):
arguments = dict(
allow_no_value=self.allow_no_value,
delimiters=self.delimiters,
comment_prefixes=self.comment_prefixes,
empty_lines_in_values=self.empty_lines_in_values,
dict_type=self.dict_type,
)
if defaults is None:
self.cf = self.config_class(allow_no_value=self.allow_no_value)
self.cf = self.config_class(**arguments)
else:
self.cf = self.config_class(defaults,
allow_no_value=self.allow_no_value)
**arguments)
return self.cf
def fromstring(self, string, defaults=None):
@ -40,27 +52,33 @@ class TestCaseBase(unittest.TestCase):
cf.readfp(sio)
return cf
class BasicTestCase(CfgParserTestCaseClass):
def test_basic(self):
config_string = (
"[Foo Bar]\n"
"foo=bar\n"
"[Spacey Bar]\n"
"foo = bar\n"
"[Commented Bar]\n"
"foo: bar ; comment\n"
"[Long Line]\n"
"foo: this line is much, much longer than my editor\n"
" likes it.\n"
"[Section\\with$weird%characters[\t]\n"
"[Internationalized Stuff]\n"
"foo[bg]: Bulgarian\n"
"foo=Default\n"
"foo[en]=English\n"
"foo[de]=Deutsch\n"
"[Spaces]\n"
"key with spaces : value\n"
"another with spaces = splat!\n"
)
config_string = """\
[Foo Bar]
foo{0[0]}bar
[Spacey Bar]
foo {0[0]} bar
[Spacey Bar From The Beginning]
foo {0[0]} bar
baz {0[0]} qwe
[Commented Bar]
foo{0[1]} bar {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
likes it.
[Section\\with$weird%characters[\t]
[Internationalized Stuff]
foo[bg]{0[1]} Bulgarian
foo{0[0]}Default
foo[en]{0[0]}English
foo[de]{0[0]}Deutsch
[Spaces]
key with spaces {0[1]} value
another with spaces {0[0]} splat!
""".format(self.delimiters, self.comment_prefixes)
if self.allow_no_value:
config_string += (
"[NoValue]\n"
@ -70,13 +88,14 @@ class TestCaseBase(unittest.TestCase):
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',
E = ['Commented Bar',
'Foo Bar',
'Internationalized Stuff',
'Long Line',
'Section\\with$weird%characters[\t',
'Spaces',
'Spacey Bar',
'Spacey Bar From The Beginning',
]
if self.allow_no_value:
E.append(r'NoValue')
@ -89,7 +108,10 @@ class TestCaseBase(unittest.TestCase):
# 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')
eq(cf.get('Spacey Bar From The Beginning', 'baz'), 'qwe')
eq(cf.get('Commented Bar', 'foo'), 'bar')
eq(cf.get('Commented Bar', 'baz'), 'qwe')
eq(cf.get('Spaces', 'key with spaces'), 'value')
eq(cf.get('Spaces', 'another with spaces'), 'splat!')
if self.allow_no_value:
@ -140,12 +162,14 @@ class TestCaseBase(unittest.TestCase):
# SF bug #432369:
cf = self.fromstring(
"[MySection]\nOption: first line\n\tsecond line\n")
"[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")
# SF bug #561822:
cf = self.fromstring("[section]\nnekey=nevalue\n",
cf = self.fromstring("[section]\n"
"nekey{}nevalue\n".format(self.delimiters[0]),
defaults={"key":"value"})
self.assertTrue(cf.has_option("section", "Key"))
@ -162,18 +186,19 @@ class TestCaseBase(unittest.TestCase):
def test_parse_errors(self):
self.newconfig()
e = self.parse_error(configparser.ParsingError,
"[Foo]\n extra-spaces: splat\n")
self.assertEqual(e.args, ('<???>',))
self.parse_error(configparser.ParsingError,
"[Foo]\n extra-spaces= splat\n")
"[Foo]\n"
"{}val-without-opt-name\n".format(self.delimiters[0]))
self.parse_error(configparser.ParsingError,
"[Foo]\n:value-without-option-name\n")
self.parse_error(configparser.ParsingError,
"[Foo]\n=value-without-option-name\n")
"[Foo]\n"
"{}val-without-opt-name\n".format(self.delimiters[1]))
e = self.parse_error(configparser.MissingSectionHeaderError,
"No Section!\n")
self.assertEqual(e.args, ('<???>', 1, "No Section!\n"))
if not self.allow_no_value:
e = self.parse_error(configparser.ParsingError,
"[Foo]\n wrong-indent\n")
self.assertEqual(e.args, ('<???>',))
def parse_error(self, exc, src):
sio = io.StringIO(src)
@ -188,9 +213,9 @@ class TestCaseBase(unittest.TestCase):
self.assertFalse(cf.has_section("Foo"),
"new ConfigParser should have no acknowledged "
"sections")
with self.assertRaises(configparser.NoSectionError) as cm:
with self.assertRaises(configparser.NoSectionError):
cf.options("Foo")
with self.assertRaises(configparser.NoSectionError) as cm:
with self.assertRaises(configparser.NoSectionError):
cf.set("foo", "bar", "value")
e = self.get_error(configparser.NoSectionError, "foo", "bar")
self.assertEqual(e.args, ("foo",))
@ -210,21 +235,21 @@ class TestCaseBase(unittest.TestCase):
def test_boolean(self):
cf = self.fromstring(
"[BOOLTEST]\n"
"T1=1\n"
"T2=TRUE\n"
"T3=True\n"
"T4=oN\n"
"T5=yes\n"
"F1=0\n"
"F2=FALSE\n"
"F3=False\n"
"F4=oFF\n"
"F5=nO\n"
"E1=2\n"
"E2=foo\n"
"E3=-1\n"
"E4=0.1\n"
"E5=FALSE AND MORE"
"T1{equals}1\n"
"T2{equals}TRUE\n"
"T3{equals}True\n"
"T4{equals}oN\n"
"T5{equals}yes\n"
"F1{equals}0\n"
"F2{equals}FALSE\n"
"F3{equals}False\n"
"F4{equals}oFF\n"
"F5{equals}nO\n"
"E1{equals}2\n"
"E2{equals}foo\n"
"E3{equals}-1\n"
"E4{equals}0.1\n"
"E5{equals}FALSE AND MORE".format(equals=self.delimiters[0])
)
for x in range(1, 5):
self.assertTrue(cf.getboolean('BOOLTEST', 't%d' % x))
@ -242,11 +267,17 @@ class TestCaseBase(unittest.TestCase):
def test_write(self):
config_string = (
"[Long Line]\n"
"foo: this line is much, much longer than my editor\n"
"foo{0[0]} this line is much, much longer than my editor\n"
" likes it.\n"
"[DEFAULT]\n"
"foo: another very\n"
"foo{0[1]} another very\n"
" long line\n"
"[Long Line - With Comments!]\n"
"test {0[1]} we {comment} can\n"
" also {comment} place\n"
" comments {comment} in\n"
" multiline {comment} values"
"\n".format(self.delimiters, comment=self.comment_prefixes[0])
)
if self.allow_no_value:
config_string += (
@ -259,13 +290,19 @@ class TestCaseBase(unittest.TestCase):
cf.write(output)
expect_string = (
"[DEFAULT]\n"
"foo = another very\n"
"foo {equals} another very\n"
"\tlong line\n"
"\n"
"[Long Line]\n"
"foo = this line is much, much longer than my editor\n"
"foo {equals} this line is much, much longer than my editor\n"
"\tlikes it.\n"
"\n"
"[Long Line - With Comments!]\n"
"test {equals} we\n"
"\talso\n"
"\tcomments\n"
"\tmultiline\n"
"\n".format(equals=self.delimiters[0])
)
if self.allow_no_value:
expect_string += (
@ -277,7 +314,7 @@ class TestCaseBase(unittest.TestCase):
def test_set_string_types(self):
cf = self.fromstring("[sect]\n"
"option1=foo\n")
"option1{eq}foo\n".format(eq=self.delimiters[0]))
# Check that we don't get an exception when setting values in
# an existing section using strings:
class mystr(str):
@ -290,6 +327,9 @@ class TestCaseBase(unittest.TestCase):
cf.set("sect", "option2", "splat")
def test_read_returns_file_list(self):
if self.delimiters[0] != '=':
# skip reading the file if we're using an incompatible format
return
file1 = support.findfile("cfgparser.1")
# check when we pass a mix of readable and non-readable files:
cf = self.newconfig()
@ -314,45 +354,45 @@ class TestCaseBase(unittest.TestCase):
def get_interpolation_config(self):
return self.fromstring(
"[Foo]\n"
"bar=something %(with1)s interpolation (1 step)\n"
"bar9=something %(with9)s lots of interpolation (9 steps)\n"
"bar10=something %(with10)s lots of interpolation (10 steps)\n"
"bar11=something %(with11)s lots of interpolation (11 steps)\n"
"with11=%(with10)s\n"
"with10=%(with9)s\n"
"with9=%(with8)s\n"
"with8=%(With7)s\n"
"with7=%(WITH6)s\n"
"with6=%(with5)s\n"
"With5=%(with4)s\n"
"WITH4=%(with3)s\n"
"with3=%(with2)s\n"
"with2=%(with1)s\n"
"with1=with\n"
"bar{equals}something %(with1)s interpolation (1 step)\n"
"bar9{equals}something %(with9)s lots of interpolation (9 steps)\n"
"bar10{equals}something %(with10)s lots of interpolation (10 steps)\n"
"bar11{equals}something %(with11)s lots of interpolation (11 steps)\n"
"with11{equals}%(with10)s\n"
"with10{equals}%(with9)s\n"
"with9{equals}%(with8)s\n"
"with8{equals}%(With7)s\n"
"with7{equals}%(WITH6)s\n"
"with6{equals}%(with5)s\n"
"With5{equals}%(with4)s\n"
"WITH4{equals}%(with3)s\n"
"with3{equals}%(with2)s\n"
"with2{equals}%(with1)s\n"
"with1{equals}with\n"
"\n"
"[Mutual Recursion]\n"
"foo=%(bar)s\n"
"bar=%(foo)s\n"
"foo{equals}%(bar)s\n"
"bar{equals}%(foo)s\n"
"\n"
"[Interpolation Error]\n"
"name=%(reference)s\n",
"name{equals}%(reference)s\n".format(equals=self.delimiters[0]),
# no definition for 'reference'
defaults={"getname": "%(__name__)s"})
def check_items_config(self, expected):
cf = self.fromstring(
"[section]\n"
"name = value\n"
"key: |%(name)s| \n"
"getdefault: |%(default)s|\n"
"getname: |%(__name__)s|",
"name {0[0]} value\n"
"key{0[1]} |%(name)s| \n"
"getdefault{0[1]} |%(default)s|\n"
"getname{0[1]} |%(__name__)s|".format(self.delimiters),
defaults={"default": "<default>"})
L = list(cf.items("section"))
L.sort()
self.assertEqual(L, expected)
class ConfigParserTestCase(TestCaseBase):
class ConfigParserTestCase(BasicTestCase):
config_class = configparser.ConfigParser
def test_interpolation(self):
@ -414,7 +454,11 @@ class ConfigParserTestCase(TestCaseBase):
self.assertRaises(ValueError, cf.get, 'non-string',
'string_with_interpolation', raw=False)
class MultilineValuesTestCase(TestCaseBase):
class ConfigParserTestCaseNonStandardDelimiters(ConfigParserTestCase):
delimiters = (':=', '$')
comment_prefixes = ('//', '"')
class MultilineValuesTestCase(BasicTestCase):
config_class = configparser.ConfigParser
wonderful_spam = ("I'm having spam spam spam spam "
"spam spam spam beaked beans spam "
@ -442,7 +486,7 @@ class MultilineValuesTestCase(TestCaseBase):
self.assertEqual(cf_from_file.get('section8', 'lovely_spam4'),
self.wonderful_spam.replace('\t\n', '\n'))
class RawConfigParserTestCase(TestCaseBase):
class RawConfigParserTestCase(BasicTestCase):
config_class = configparser.RawConfigParser
def test_interpolation(self):
@ -476,6 +520,28 @@ class RawConfigParserTestCase(TestCaseBase):
[0, 1, 1, 2, 3, 5, 8, 13])
self.assertEqual(cf.get('non-string', 'dict'), {'pi': 3.14159})
class RawConfigParserTestCaseNonStandardDelimiters(RawConfigParserTestCase):
delimiters = (':=', '$')
comment_prefixes = ('//', '"')
class RawConfigParserTestSambaConf(BasicTestCase):
config_class = configparser.RawConfigParser
comment_prefixes = ('#', ';', '//', '----')
empty_lines_in_values = False
def test_reading(self):
smbconf = support.findfile("cfgparser.2")
# check when we pass a mix of readable and non-readable files:
cf = self.newconfig()
parsed_files = cf.read([smbconf, "nonexistent-file"])
self.assertEqual(parsed_files, [smbconf])
sections = ['global', 'homes', 'printers',
'print$', 'pdf-generator', 'tmp', 'Agustin']
self.assertEqual(cf.sections(), sections)
self.assertEqual(cf.get("global", "workgroup"), "MDKGROUP")
self.assertEqual(cf.getint("global", "max log size"), 50)
self.assertEqual(cf.get("global", "hosts allow"), "127.")
self.assertEqual(cf.get("tmp", "echo command"), "cat %s; rm %s")
class SafeConfigParserTestCase(ConfigParserTestCase):
config_class = configparser.SafeConfigParser
@ -483,16 +549,17 @@ class SafeConfigParserTestCase(ConfigParserTestCase):
def test_safe_interpolation(self):
# See http://www.python.org/sf/511737
cf = self.fromstring("[section]\n"
"option1=xxx\n"
"option2=%(option1)s/xxx\n"
"ok=%(option1)s/%%s\n"
"not_ok=%(option2)s/%%s")
"option1{eq}xxx\n"
"option2{eq}%(option1)s/xxx\n"
"ok{eq}%(option1)s/%%s\n"
"not_ok{eq}%(option2)s/%%s".format(
eq=self.delimiters[0]))
self.assertEqual(cf.get("section", "ok"), "xxx/%s")
self.assertEqual(cf.get("section", "not_ok"), "xxx/xxx/%s")
def test_set_malformatted_interpolation(self):
cf = self.fromstring("[sect]\n"
"option1=foo\n")
"option1{eq}foo\n".format(eq=self.delimiters[0]))
self.assertEqual(cf.get('sect', "option1"), "foo")
@ -508,7 +575,7 @@ class SafeConfigParserTestCase(ConfigParserTestCase):
def test_set_nonstring_types(self):
cf = self.fromstring("[sect]\n"
"option1=foo\n")
"option1{eq}foo\n".format(eq=self.delimiters[0]))
# Check that we get a TypeError when setting non-string values
# in an existing section:
self.assertRaises(TypeError, cf.set, "sect", "option1", 1)
@ -526,15 +593,16 @@ class SafeConfigParserTestCase(ConfigParserTestCase):
cf = self.newconfig()
self.assertRaises(ValueError, cf.add_section, "DEFAULT")
class SafeConfigParserTestCaseNonStandardDelimiters(SafeConfigParserTestCase):
delimiters = (':=', '$')
comment_prefixes = ('//', '"')
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)
return self.cf
dict_type = SortedDict
def test_sorted(self):
self.fromstring("[b]\n"
@ -556,14 +624,36 @@ class SortedTestCase(RawConfigParserTestCase):
"o4 = 1\n\n")
class CompatibleTestCase(CfgParserTestCaseClass):
config_class = configparser.RawConfigParser
comment_prefixes = configparser.RawConfigParser._COMPATIBLE
def test_comment_handling(self):
config_string = textwrap.dedent("""\
[Commented Bar]
baz=qwe ; a comment
foo: bar # not a comment!
# but this is a comment
; another comment
""")
cf = self.fromstring(config_string)
self.assertEqual(cf.get('Commented Bar', 'foo'), 'bar # not a comment!')
self.assertEqual(cf.get('Commented Bar', 'baz'), 'qwe')
def test_main():
support.run_unittest(
ConfigParserTestCase,
ConfigParserTestCaseNonStandardDelimiters,
MultilineValuesTestCase,
RawConfigParserTestCase,
RawConfigParserTestCaseNonStandardDelimiters,
RawConfigParserTestSambaConf,
SafeConfigParserTestCase,
SafeConfigParserTestCaseNonStandardDelimiters,
SafeConfigParserTestCaseNoValue,
SortedTestCase,
CompatibleTestCase,
)