mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
[3.12] gh-116957: configparser: Do post-process values after DuplicateOptionError (GH-116958) (GH-117013)
If you catch DuplicateOptionError / DuplicateSectionError when reading a
config file (the intention is to skip invalid config files) and then
attempt to use the ConfigParser instance, any values it *had* read
successfully so far, were stored as a list instead of string! Later
`get` calls would raise "AttributeError: 'list' object has no attribute
'find'" from somewhere deep in the interpolation code.
(cherry picked from commit b1bc37597f
)
This commit is contained in:
parent
688623d402
commit
0fc8ae4e28
3 changed files with 109 additions and 89 deletions
|
@ -995,100 +995,102 @@ class RawConfigParser(MutableMapping):
|
||||||
lineno = 0
|
lineno = 0
|
||||||
indent_level = 0
|
indent_level = 0
|
||||||
e = None # None, or an exception
|
e = None # None, or an exception
|
||||||
for lineno, line in enumerate(fp, start=1):
|
try:
|
||||||
comment_start = sys.maxsize
|
for lineno, line in enumerate(fp, start=1):
|
||||||
# strip inline comments
|
comment_start = sys.maxsize
|
||||||
inline_prefixes = {p: -1 for p in self._inline_comment_prefixes}
|
# strip inline comments
|
||||||
while comment_start == sys.maxsize and inline_prefixes:
|
inline_prefixes = {p: -1 for p in self._inline_comment_prefixes}
|
||||||
next_prefixes = {}
|
while comment_start == sys.maxsize and inline_prefixes:
|
||||||
for prefix, index in inline_prefixes.items():
|
next_prefixes = {}
|
||||||
index = line.find(prefix, index+1)
|
for prefix, index in inline_prefixes.items():
|
||||||
if index == -1:
|
index = line.find(prefix, index+1)
|
||||||
continue
|
if index == -1:
|
||||||
next_prefixes[prefix] = index
|
continue
|
||||||
if index == 0 or (index > 0 and line[index-1].isspace()):
|
next_prefixes[prefix] = index
|
||||||
comment_start = min(comment_start, index)
|
if index == 0 or (index > 0 and line[index-1].isspace()):
|
||||||
inline_prefixes = next_prefixes
|
comment_start = min(comment_start, index)
|
||||||
# strip full line comments
|
inline_prefixes = next_prefixes
|
||||||
for prefix in self._comment_prefixes:
|
# strip full line comments
|
||||||
if line.strip().startswith(prefix):
|
for prefix in self._comment_prefixes:
|
||||||
comment_start = 0
|
if line.strip().startswith(prefix):
|
||||||
break
|
comment_start = 0
|
||||||
if comment_start == sys.maxsize:
|
break
|
||||||
comment_start = None
|
if comment_start == sys.maxsize:
|
||||||
value = line[:comment_start].strip()
|
comment_start = None
|
||||||
if not value:
|
value = line[:comment_start].strip()
|
||||||
if self._empty_lines_in_values:
|
if not value:
|
||||||
# add empty line to the value, but only if there was no
|
if self._empty_lines_in_values:
|
||||||
# comment on the line
|
# add empty line to the value, but only if there was no
|
||||||
if (comment_start is None and
|
# comment on the line
|
||||||
cursect is not None and
|
if (comment_start is None and
|
||||||
optname and
|
cursect is not None and
|
||||||
cursect[optname] is not None):
|
optname and
|
||||||
cursect[optname].append('') # newlines added at join
|
cursect[optname] is not None):
|
||||||
else:
|
cursect[optname].append('') # newlines added at join
|
||||||
# empty line marks end of value
|
|
||||||
indent_level = sys.maxsize
|
|
||||||
continue
|
|
||||||
# continuation line?
|
|
||||||
first_nonspace = self.NONSPACECRE.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(value)
|
|
||||||
if mo:
|
|
||||||
sectname = mo.group('header')
|
|
||||||
if sectname in self._sections:
|
|
||||||
if self._strict and sectname in elements_added:
|
|
||||||
raise DuplicateSectionError(sectname, fpname,
|
|
||||||
lineno)
|
|
||||||
cursect = self._sections[sectname]
|
|
||||||
elements_added.add(sectname)
|
|
||||||
elif sectname == self.default_section:
|
|
||||||
cursect = self._defaults
|
|
||||||
else:
|
else:
|
||||||
cursect = self._dict()
|
# empty line marks end of value
|
||||||
self._sections[sectname] = cursect
|
indent_level = sys.maxsize
|
||||||
self._proxies[sectname] = SectionProxy(self, sectname)
|
continue
|
||||||
elements_added.add(sectname)
|
# continuation line?
|
||||||
# So sections can't start with a continuation line
|
first_nonspace = self.NONSPACECRE.search(line)
|
||||||
optname = None
|
cur_indent_level = first_nonspace.start() if first_nonspace else 0
|
||||||
# no section header in the file?
|
if (cursect is not None and optname and
|
||||||
elif cursect is None:
|
cur_indent_level > indent_level):
|
||||||
raise MissingSectionHeaderError(fpname, lineno, line)
|
cursect[optname].append(value)
|
||||||
# an option line?
|
# a section header or option header?
|
||||||
else:
|
else:
|
||||||
mo = self._optcre.match(value)
|
indent_level = cur_indent_level
|
||||||
|
# is it a section header?
|
||||||
|
mo = self.SECTCRE.match(value)
|
||||||
if mo:
|
if mo:
|
||||||
optname, vi, optval = mo.group('option', 'vi', 'value')
|
sectname = mo.group('header')
|
||||||
if not optname:
|
if sectname in self._sections:
|
||||||
e = self._handle_error(e, fpname, lineno, line)
|
if self._strict and sectname in elements_added:
|
||||||
optname = self.optionxform(optname.rstrip())
|
raise DuplicateSectionError(sectname, fpname,
|
||||||
if (self._strict and
|
lineno)
|
||||||
(sectname, optname) in elements_added):
|
cursect = self._sections[sectname]
|
||||||
raise DuplicateOptionError(sectname, optname,
|
elements_added.add(sectname)
|
||||||
fpname, lineno)
|
elif sectname == self.default_section:
|
||||||
elements_added.add((sectname, optname))
|
cursect = self._defaults
|
||||||
# This check is fine because the OPTCRE cannot
|
|
||||||
# match if it would set optval to None
|
|
||||||
if optval is not None:
|
|
||||||
optval = optval.strip()
|
|
||||||
cursect[optname] = [optval]
|
|
||||||
else:
|
else:
|
||||||
# valueless option handling
|
cursect = self._dict()
|
||||||
cursect[optname] = None
|
self._sections[sectname] = cursect
|
||||||
|
self._proxies[sectname] = SectionProxy(self, sectname)
|
||||||
|
elements_added.add(sectname)
|
||||||
|
# So sections can't start with a continuation line
|
||||||
|
optname = None
|
||||||
|
# no section header in the file?
|
||||||
|
elif cursect is None:
|
||||||
|
raise MissingSectionHeaderError(fpname, lineno, line)
|
||||||
|
# an option line?
|
||||||
else:
|
else:
|
||||||
# a non-fatal parsing error occurred. set up the
|
mo = self._optcre.match(value)
|
||||||
# exception but keep going. the exception will be
|
if mo:
|
||||||
# raised at the end of the file and will contain a
|
optname, vi, optval = mo.group('option', 'vi', 'value')
|
||||||
# list of all bogus lines
|
if not optname:
|
||||||
e = self._handle_error(e, fpname, lineno, line)
|
e = self._handle_error(e, fpname, lineno, line)
|
||||||
self._join_multiline_values()
|
optname = self.optionxform(optname.rstrip())
|
||||||
|
if (self._strict and
|
||||||
|
(sectname, optname) in elements_added):
|
||||||
|
raise DuplicateOptionError(sectname, optname,
|
||||||
|
fpname, lineno)
|
||||||
|
elements_added.add((sectname, optname))
|
||||||
|
# This check is fine because the OPTCRE cannot
|
||||||
|
# match if it would set optval to None
|
||||||
|
if optval is not None:
|
||||||
|
optval = optval.strip()
|
||||||
|
cursect[optname] = [optval]
|
||||||
|
else:
|
||||||
|
# valueless option handling
|
||||||
|
cursect[optname] = None
|
||||||
|
else:
|
||||||
|
# a non-fatal parsing error occurred. set up the
|
||||||
|
# exception but keep going. the exception will be
|
||||||
|
# raised at the end of the file and will contain a
|
||||||
|
# list of all bogus lines
|
||||||
|
e = self._handle_error(e, fpname, lineno, line)
|
||||||
|
finally:
|
||||||
|
self._join_multiline_values()
|
||||||
# if any parsing errors occurred, raise an exception
|
# if any parsing errors occurred, raise an exception
|
||||||
if e:
|
if e:
|
||||||
raise e
|
raise e
|
||||||
|
|
|
@ -647,6 +647,21 @@ boolean {0[0]} NO
|
||||||
"'opt' in section 'Bar' already exists")
|
"'opt' in section 'Bar' already exists")
|
||||||
self.assertEqual(e.args, ("Bar", "opt", "<dict>", None))
|
self.assertEqual(e.args, ("Bar", "opt", "<dict>", None))
|
||||||
|
|
||||||
|
def test_get_after_duplicate_option_error(self):
|
||||||
|
cf = self.newconfig()
|
||||||
|
ini = textwrap.dedent("""\
|
||||||
|
[Foo]
|
||||||
|
x{equals}1
|
||||||
|
y{equals}2
|
||||||
|
y{equals}3
|
||||||
|
""".format(equals=self.delimiters[0]))
|
||||||
|
if self.strict:
|
||||||
|
with self.assertRaises(configparser.DuplicateOptionError):
|
||||||
|
cf.read_string(ini)
|
||||||
|
else:
|
||||||
|
cf.read_string(ini)
|
||||||
|
self.assertEqual(cf.get('Foo', 'x'), '1')
|
||||||
|
|
||||||
def test_write(self):
|
def test_write(self):
|
||||||
config_string = (
|
config_string = (
|
||||||
"[Long Line]\n"
|
"[Long Line]\n"
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
configparser: Don't leave ConfigParser values in an invalid state (stored as
|
||||||
|
a list instead of a str) after an earlier read raised DuplicateSectionError
|
||||||
|
or DuplicateOptionError.
|
Loading…
Add table
Add a link
Reference in a new issue