[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:
David Röthlisberger 2024-03-19 17:18:50 +00:00 committed by GitHub
parent 688623d402
commit 0fc8ae4e28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 109 additions and 89 deletions

View file

@ -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

View file

@ -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"

View file

@ -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.