Index: Lib/configparser.py
===================================================================
--- Lib/configparser.py (revision 83161)
+++ Lib/configparser.py (working copy)
@@ -24,12 +24,24 @@
methods:
- __init__(defaults=None)
- create the parser and specify a dictionary of intrinsic defaults. The
- keys must be strings, the values must be appropriate for %()s string
- interpolation. Note that `__name__' is always an intrinsic default;
- its value is the section's name.
+ __init__(defaults=None, dict_type=collections.OrderedDict,
+ delimiters=('=', ':'), allow_no_value=False)
+ create the parser. When `defaults' is given, it is initialized into the
+ dictionary or intrinsic defaults. The keys must be strings, the values
+ must be appropriate for %()s string interpolation. Note that `__name__'
+ is always an intrinsic default; its value is the section's name.
+
+ 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.
+
+ When `delimiters' is given, it will be used as the set of substrings
+ that divide keys from values.
+
+ When `allow_no_value' is `True' (default: `False'), options without
+ values are accepted; the value presented for these is ``None``.
+
sections()
return all the configuration section names, sans DEFAULT
@@ -94,6 +106,8 @@
_default_dict = dict
import re
+import sre_parse
+import sys
__all__ = ["NoSectionError", "DuplicateSectionError", "NoOptionError",
"InterpolationError", "InterpolationDepthError",
@@ -229,17 +243,27 @@
class RawConfigParser:
def __init__(self, defaults=None, dict_type=_default_dict,
- allow_no_value=False):
+ delimiters=('=', ':'), comment_prefixes=('#', ';'),
+ empty_lines_in_values=True, 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
+ self._delimiters = tuple(delimiters)
+ if delimiters != ('=', ':'):
+ spec = sre_parse.SPECIAL_CHARS
+ escape = lambda d: d if d not in spec else r'\{}'.format(d)
+ delim = "|".join((escape(d) for d in delimiters))
+ self.OPTCRE = re.compile(self._OPT_TMPL.format(delim=delim),
+ re.VERBOSE)
+ self.OPTCRE_NV = re.compile(self._OPT_NV_TMPL.format(delim=delim),
+ re.VERBOSE)
+ self._comment_prefixes = tuple(comment_prefixes)
+ self._empty_lines_in_values = empty_lines_in_values
+ if allow_no_value:
+ self.OPTCRE = self.OPTCRE_NV
def defaults(self):
return self._defaults
@@ -395,12 +419,17 @@
raise NoSectionError(section)
sectdict[self.optionxform(option)] = value
- def write(self, fp):
+ def write(self, fp, space_around_delimiters=True):
"""Write an .ini-format representation of the configuration state."""
+ if space_around_delimiters:
+ d = " {} ".format(self._delimiters[0])
+ else:
+ d = self._delimiters[0]
if self._defaults:
fp.write("[%s]\n" % DEFAULTSECT)
for (key, value) in self._defaults.items():
- fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t')))
+ fp.write("%s%s%s\n" % (key, d,
+ str(value).replace('\n', '\n\t')))
fp.write("\n")
for section in self._sections:
fp.write("[%s]\n" % section)
@@ -408,7 +437,7 @@
if key == "__name__":
continue
if value is not None:
- key = " = ".join((key, str(value).replace('\n', '\n\t')))
+ key = d.join((key, str(value).replace('\n', '\n\t')))
fp.write("%s\n" % (key))
fp.write("\n")
@@ -437,28 +466,33 @@
#
# Regular expressions for parsing section headers and options.
#
- SECTCRE = re.compile(
- r'\[' # [
- r'(?P
[^]]+)' # very permissive!
- r'\]' # ]
- )
- OPTCRE = re.compile(
- r'(?P[^:=\s][^:=]*)' # very permissive!
- r'\s*(?P[:=])\s*' # any number of space/tab,
- # followed by separator
- # (either : or =), followed
- # by any # space/tab
- r'(?P.*)$' # everything up to eol
- )
- OPTCRE_NV = re.compile(
- r'(?P[^:=\s][^:=]*)' # very permissive!
- r'\s*(?:' # any number of space/tab,
- r'(?P[:=])\s*' # optionally followed by
- # separator (either : or
- # =), followed by any #
- # space/tab
- r'(?P.*))?$' # everything up to eol
- )
+ _SECT_TMPL = r"""
+ \[ # [
+ (?P[^]]+) # very permissive!
+ \] # ]
+ """
+ _OPT_TMPL = r"""
+ (?P.*?) # very permissive!
+ \s*(?P{delim})\s* # any number of space/tab,
+ # followed by any of the
+ # allowed delimiters,
+ # followed by any space/tab
+ (?P.*)$ # everything up to eol
+ """
+ _OPT_NV_TMPL = r"""
+ (?P.*?) # very permissive!
+ \s*(?: # any number of space/tab,
+ (?P{delim})\s* # optionally followed by
+ # any of the allowed
+ # delimiters, followed by any
+ # space/tab
+ (?P.*))?$ # everything up to eol
+ """
+
+ SECTCRE = re.compile(_SECT_TMPL, re.VERBOSE)
+ OPTCRE = re.compile(_OPT_TMPL.format(delim=r"=|:"), re.VERBOSE)
+ OPTCRE_NV = re.compile(_OPT_NV_TMPL.format(delim=r"=|:"), re.VERBOSE)
+ INDENTCRE = re.compile(r"\S")
def _read(self, fp, fpname):
"""Parse a sectioned setup file.
@@ -470,30 +504,46 @@
leading whitespace. Blank lines, lines beginning with a '#',
and just about everything else are ignored.
"""
- cursect = None # None, or a dictionary
+ cursect = None # None, or a dictionary
optname = None
lineno = 0
- e = None # None, or an exception
+ indent_level = 0
+ e = None # None, or an exception
while True:
line = fp.readline()
if not line:
break
lineno = lineno + 1
- # comment or blank line?
- if line.strip() == '' or line[0] in '#;':
+ # strip comments
+ strip_from = None
+ for prefix in self._comment_prefixes:
+ index = line.find(prefix)
+ # comment delimiters after values must follow a spacing
+ # character
+ if index == 0 or (index> 0 and line[index-1].isspace()):
+ strip_from = index
+ break
+ value = line[:strip_from].strip()
+ if not value:
+ if self._empty_lines_in_values:
+ # add empty line to the value
+ if cursect is not None and optname:
+ cursect[optname].append('\n')
+ else:
+ # empty line marks end of value
+ indent_level = sys.maxsize
continue
- if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
- # no leading whitespace
- continue
# continuation line?
- if line[0].isspace() and cursect is not None and optname:
- value = line.strip()
- if value:
- cursect[optname].append(value)
+ first_nonspace = self.INDENTCRE.search(line)
+ cur_indent_level = first_nonspace.start() if first_nonspace else 0
+ if cursect is not None and optname and \
+ cur_indent_level> indent_level:
+ cursect[optname].append(value)
# a section header or option header?
else:
+ indent_level = cur_indent_level
# is it a section header?
- mo = self.SECTCRE.match(line)
+ mo = self.SECTCRE.match(value)
if mo:
sectname = mo.group('header')
if sectname in self._sections:
@@ -511,19 +561,15 @@
raise MissingSectionHeaderError(fpname, lineno, line)
# an option line?
else:
- mo = self._optcre.match(line)
+ mo = self.OPTCRE.match(value)
if mo:
optname, vi, optval = mo.group('option', 'vi', 'value')
+ if not optname:
+ e = self._handle_error(e, fpname, lineno, line)
optname = self.optionxform(optname.rstrip())
# 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 == '""':
@@ -537,21 +583,28 @@
# exception but keep going. the exception will be
# raised at the end of the file and will contain a
# list of all bogus lines
- if not e:
- e = ParsingError(fpname)
- e.append(lineno, repr(line))
+ e = self._handle_error(e, fpname, lineno, line)
# if any parsing errors occurred, raise an exception
if e:
raise e
+ self._join_multiline_values()
- # join the multi-line values collected while reading
+ def _join_multiline_values(self):
all_sections = [self._defaults]
all_sections.extend(self._sections.values())
for options in all_sections:
for name, val in options.items():
if isinstance(val, list):
+ if val[-1] == '\n':
+ val = val[:-1]
options[name] = '\n'.join(val)
+ def _handle_error(self, exc, fpname, lineno, line):
+ if not exc:
+ exc = ParsingError(fpname)
+ exc.append(lineno, repr(line))
+ return exc
+
class ConfigParser(RawConfigParser):
def get(self, section, option, raw=False, vars=None):
@@ -702,7 +755,7 @@
# 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 self.OPTCRE is not self.OPTCRE_NV or value:
if not isinstance(value, str):
raise TypeError("option values must be strings")
# check for bad percent signs:
Index: Lib/test/test_cfgparser.py
===================================================================
--- Lib/test/test_cfgparser.py (revision 83161)
+++ Lib/test/test_cfgparser.py (working copy)
@@ -25,13 +25,22 @@
class TestCaseBase(unittest.TestCase):
allow_no_value = False
-
+ delimiters = ('=', ':')
+ comment_prefixes = ('#', ';')
+ empty_lines_in_values = True
+
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
+ )
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):
@@ -43,23 +52,28 @@
def test_basic(self):
config_string = (
"[Foo Bar]\n"
- "foo=bar\n"
+ "foo{0[0]}bar\n"
"[Spacey Bar]\n"
- "foo = bar\n"
+ "foo {0[0]} bar\n"
+ "[Spacey Bar From The Beginning]\n"
+ " foo {0[0]} bar\n"
+ " baz {0[0]} qwe\n"
"[Commented Bar]\n"
- "foo: bar ; comment\n"
+ "foo{0[1]} bar {1[1]} comment\n"
+ "baz{0[0]}qwe {1[0]}another one\n"
"[Long Line]\n"
- "foo: this line is much, much longer than my editor\n"
+ "foo{0[1]} 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"
+ "foo[bg]{0[1]} Bulgarian\n"
+ "foo{0[0]}Default\n"
+ "foo[en]{0[0]}English\n"
+ "foo[de]{0[0]}Deutsch\n"
"[Spaces]\n"
- "key with spaces : value\n"
- "another with spaces = splat!\n"
+ "key with spaces {0[1]} value\n"
+ "another with spaces {0[0]} splat!\n".format(self.delimiters,
+ self.comment_prefixes)
)
if self.allow_no_value:
config_string += (
@@ -77,6 +91,7 @@
r'Section\with$weird%characters[' '\t',
r'Spaces',
r'Spacey Bar',
+ r'Spacey Bar From The Beginning',
]
if self.allow_no_value:
E.append(r'NoValue')
@@ -89,7 +104,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 +158,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 +182,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)
@@ -210,21 +231,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 +263,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 +286,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 +310,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 +323,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,38 +350,38 @@
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": ""})
L = list(cf.items("section"))
L.sort()
@@ -414,6 +450,10 @@
self.assertRaises(ValueError, cf.get, 'non-string',
'string_with_interpolation', raw=False)
+class ConfigParserTestCaseNonStandardDelimiters(ConfigParserTestCase):
+ delimiters = (':=', '$')
+ comment_prefixes = ('//', '"')
+
class MultilineValuesTestCase(TestCaseBase):
config_class = configparser.ConfigParser
wonderful_spam = ("I'm having spam spam spam spam "
@@ -476,23 +516,46 @@
[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(TestCaseBase):
+ 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
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 +571,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,6 +589,9 @@
cf = self.newconfig()
self.assertRaises(ValueError, cf.add_section, "DEFAULT")
+class SafeConfigParserTestCaseNonStandardDelimiters(SafeConfigParserTestCase):
+ delimiters = (':=', '$')
+ comment_prefixes = ('//', '"')
class SafeConfigParserTestCaseNoValue(SafeConfigParserTestCase):
allow_no_value = True
@@ -559,9 +625,13 @@
def test_main():
support.run_unittest(
ConfigParserTestCase,
+ ConfigParserTestCaseNonStandardDelimiters,
MultilineValuesTestCase,
RawConfigParserTestCase,
+ RawConfigParserTestCaseNonStandardDelimiters,
+ RawConfigParserTestSambaConf,
SafeConfigParserTestCase,
+ SafeConfigParserTestCaseNonStandardDelimiters,
SafeConfigParserTestCaseNoValue,
SortedTestCase,
)