#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.
diff --git a/Lib/test/test_cfgparser.py b/Lib/test/test_cfgparser.py
index c5a2595..e00a51a 100644
--- a/Lib/test/test_cfgparser.py
+++ b/Lib/test/test_cfgparser.py
@@ -3,6 +3,7 @@
import io
import os
import unittest
+import textwrap
from test import support
@@ -23,15 +24,26 @@
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 @@
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 @@
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 @@
# 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 @@
# 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
[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 @@
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 @@
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 @@
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 @@
"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,
)