Skip to content

Commit b1bc375

Browse files
authored
gh-116957: configparser: Do post-process values after DuplicateOptionError (GH-116958)
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.
1 parent a8e93d3 commit b1bc375

File tree

3 files changed

+111
-91
lines changed

3 files changed

+111
-91
lines changed

Lib/configparser.py

Lines changed: 93 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -961,102 +961,104 @@ def _read(self, fp, fpname):
961961
lineno = 0
962962
indent_level = 0
963963
e = None # None, or an exception
964-
for lineno, line in enumerate(fp, start=1):
965-
comment_start = sys.maxsize
966-
# strip inline comments
967-
inline_prefixes = {p: -1 for p in self._inline_comment_prefixes}
968-
while comment_start == sys.maxsize and inline_prefixes:
969-
next_prefixes = {}
970-
for prefix, index in inline_prefixes.items():
971-
index = line.find(prefix, index+1)
972-
if index == -1:
973-
continue
974-
next_prefixes[prefix] = index
975-
if index == 0 or (index > 0 and line[index-1].isspace()):
976-
comment_start = min(comment_start, index)
977-
inline_prefixes = next_prefixes
978-
# strip full line comments
979-
for prefix in self._comment_prefixes:
980-
if line.strip().startswith(prefix):
981-
comment_start = 0
982-
break
983-
if comment_start == sys.maxsize:
984-
comment_start = None
985-
value = line[:comment_start].strip()
986-
if not value:
987-
if self._empty_lines_in_values:
988-
# add empty line to the value, but only if there was no
989-
# comment on the line
990-
if (comment_start is None and
991-
cursect is not None and
992-
optname and
993-
cursect[optname] is not None):
994-
cursect[optname].append('') # newlines added at join
995-
else:
996-
# empty line marks end of value
997-
indent_level = sys.maxsize
998-
continue
999-
# continuation line?
1000-
first_nonspace = self.NONSPACECRE.search(line)
1001-
cur_indent_level = first_nonspace.start() if first_nonspace else 0
1002-
if (cursect is not None and optname and
1003-
cur_indent_level > indent_level):
1004-
if cursect[optname] is None:
1005-
raise MultilineContinuationError(fpname, lineno, line)
1006-
cursect[optname].append(value)
1007-
# a section header or option header?
1008-
else:
1009-
indent_level = cur_indent_level
1010-
# is it a section header?
1011-
mo = self.SECTCRE.match(value)
1012-
if mo:
1013-
sectname = mo.group('header')
1014-
if sectname in self._sections:
1015-
if self._strict and sectname in elements_added:
1016-
raise DuplicateSectionError(sectname, fpname,
1017-
lineno)
1018-
cursect = self._sections[sectname]
1019-
elements_added.add(sectname)
1020-
elif sectname == self.default_section:
1021-
cursect = self._defaults
964+
try:
965+
for lineno, line in enumerate(fp, start=1):
966+
comment_start = sys.maxsize
967+
# strip inline comments
968+
inline_prefixes = {p: -1 for p in self._inline_comment_prefixes}
969+
while comment_start == sys.maxsize and inline_prefixes:
970+
next_prefixes = {}
971+
for prefix, index in inline_prefixes.items():
972+
index = line.find(prefix, index+1)
973+
if index == -1:
974+
continue
975+
next_prefixes[prefix] = index
976+
if index == 0 or (index > 0 and line[index-1].isspace()):
977+
comment_start = min(comment_start, index)
978+
inline_prefixes = next_prefixes
979+
# strip full line comments
980+
for prefix in self._comment_prefixes:
981+
if line.strip().startswith(prefix):
982+
comment_start = 0
983+
break
984+
if comment_start == sys.maxsize:
985+
comment_start = None
986+
value = line[:comment_start].strip()
987+
if not value:
988+
if self._empty_lines_in_values:
989+
# add empty line to the value, but only if there was no
990+
# comment on the line
991+
if (comment_start is None and
992+
cursect is not None and
993+
optname and
994+
cursect[optname] is not None):
995+
cursect[optname].append('') # newlines added at join
1022996
else:
1023-
cursect = self._dict()
1024-
self._sections[sectname] = cursect
1025-
self._proxies[sectname] = SectionProxy(self, sectname)
1026-
elements_added.add(sectname)
1027-
# So sections can't start with a continuation line
1028-
optname = None
1029-
# no section header in the file?
1030-
elif cursect is None:
1031-
raise MissingSectionHeaderError(fpname, lineno, line)
1032-
# an option line?
997+
# empty line marks end of value
998+
indent_level = sys.maxsize
999+
continue
1000+
# continuation line?
1001+
first_nonspace = self.NONSPACECRE.search(line)
1002+
cur_indent_level = first_nonspace.start() if first_nonspace else 0
1003+
if (cursect is not None and optname and
1004+
cur_indent_level > indent_level):
1005+
if cursect[optname] is None:
1006+
raise MultilineContinuationError(fpname, lineno, line)
1007+
cursect[optname].append(value)
1008+
# a section header or option header?
10331009
else:
1034-
mo = self._optcre.match(value)
1010+
indent_level = cur_indent_level
1011+
# is it a section header?
1012+
mo = self.SECTCRE.match(value)
10351013
if mo:
1036-
optname, vi, optval = mo.group('option', 'vi', 'value')
1037-
if not optname:
1038-
e = self._handle_error(e, fpname, lineno, line)
1039-
optname = self.optionxform(optname.rstrip())
1040-
if (self._strict and
1041-
(sectname, optname) in elements_added):
1042-
raise DuplicateOptionError(sectname, optname,
1043-
fpname, lineno)
1044-
elements_added.add((sectname, optname))
1045-
# This check is fine because the OPTCRE cannot
1046-
# match if it would set optval to None
1047-
if optval is not None:
1048-
optval = optval.strip()
1049-
cursect[optname] = [optval]
1014+
sectname = mo.group('header')
1015+
if sectname in self._sections:
1016+
if self._strict and sectname in elements_added:
1017+
raise DuplicateSectionError(sectname, fpname,
1018+
lineno)
1019+
cursect = self._sections[sectname]
1020+
elements_added.add(sectname)
1021+
elif sectname == self.default_section:
1022+
cursect = self._defaults
10501023
else:
1051-
# valueless option handling
1052-
cursect[optname] = None
1024+
cursect = self._dict()
1025+
self._sections[sectname] = cursect
1026+
self._proxies[sectname] = SectionProxy(self, sectname)
1027+
elements_added.add(sectname)
1028+
# So sections can't start with a continuation line
1029+
optname = None
1030+
# no section header in the file?
1031+
elif cursect is None:
1032+
raise MissingSectionHeaderError(fpname, lineno, line)
1033+
# an option line?
10531034
else:
1054-
# a non-fatal parsing error occurred. set up the
1055-
# exception but keep going. the exception will be
1056-
# raised at the end of the file and will contain a
1057-
# list of all bogus lines
1058-
e = self._handle_error(e, fpname, lineno, line)
1059-
self._join_multiline_values()
1035+
mo = self._optcre.match(value)
1036+
if mo:
1037+
optname, vi, optval = mo.group('option', 'vi', 'value')
1038+
if not optname:
1039+
e = self._handle_error(e, fpname, lineno, line)
1040+
optname = self.optionxform(optname.rstrip())
1041+
if (self._strict and
1042+
(sectname, optname) in elements_added):
1043+
raise DuplicateOptionError(sectname, optname,
1044+
fpname, lineno)
1045+
elements_added.add((sectname, optname))
1046+
# This check is fine because the OPTCRE cannot
1047+
# match if it would set optval to None
1048+
if optval is not None:
1049+
optval = optval.strip()
1050+
cursect[optname] = [optval]
1051+
else:
1052+
# valueless option handling
1053+
cursect[optname] = None
1054+
else:
1055+
# a non-fatal parsing error occurred. set up the
1056+
# exception but keep going. the exception will be
1057+
# raised at the end of the file and will contain a
1058+
# list of all bogus lines
1059+
e = self._handle_error(e, fpname, lineno, line)
1060+
finally:
1061+
self._join_multiline_values()
10601062
# if any parsing errors occurred, raise an exception
10611063
if e:
10621064
raise e

Lib/test/test_configparser.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,21 @@ def test_weird_errors(self):
646646
"'opt' in section 'Bar' already exists")
647647
self.assertEqual(e.args, ("Bar", "opt", "<dict>", None))
648648

649+
def test_get_after_duplicate_option_error(self):
650+
cf = self.newconfig()
651+
ini = textwrap.dedent("""\
652+
[Foo]
653+
x{equals}1
654+
y{equals}2
655+
y{equals}3
656+
""".format(equals=self.delimiters[0]))
657+
if self.strict:
658+
with self.assertRaises(configparser.DuplicateOptionError):
659+
cf.read_string(ini)
660+
else:
661+
cf.read_string(ini)
662+
self.assertEqual(cf.get('Foo', 'x'), '1')
663+
649664
def test_write(self):
650665
config_string = (
651666
"[Long Line]\n"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
configparser: Don't leave ConfigParser values in an invalid state (stored as
2+
a list instead of a str) after an earlier read raised DuplicateSectionError
3+
or DuplicateOptionError.

0 commit comments

Comments
 (0)