mirror of
https://github.com/python/cpython.git
synced 2025-07-23 03:05:38 +00:00
#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:
parent
cbb0ae4a42
commit
96a60ae90c
4 changed files with 546 additions and 281 deletions
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue