Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b517fd5

Browse files
authored
[3.11] gh-116957: configparser: Do post-process values after DuplicateOptionError (GH-116958) (GH-117012)
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 b1bc375)
1 parent bb7a6d4 commit b517fd5

File tree

3 files changed

+109
-89
lines changed

3 files changed

+109
-89
lines changed

Lib/configparser.py

Lines changed: 91 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,100 +1033,102 @@ def _read(self, fp, fpname):
10331033
lineno = 0
10341034
indent_level = 0
10351035
e = None # None, or an exception
1036-
for lineno, line in enumerate(fp, start=1):
1037-
comment_start = sys.maxsize
1038-
# strip inline comments
1039-
inline_prefixes = {p: -1 for p in self._inline_comment_prefixes}
1040-
while comment_start == sys.maxsize and inline_prefixes:
1041-
next_prefixes = {}
1042-
for prefix, index in inline_prefixes.items():
1043-
index = line.find(prefix, index+1)
1044-
if index == -1:
1045-
continue
1046-
next_prefixes[prefix] = index
1047-
if index == 0 or (index > 0 and line[index-1].isspace()):
1048-
comment_start = min(comment_start, index)
1049-
inline_prefixes = next_prefixes
1050-
# strip full line comments
1051-
for prefix in self._comment_prefixes:
1052-
if line.strip().startswith(prefix):
1053-
comment_start = 0
1054-
break
1055-
if comment_start == sys.maxsize:
1056-
comment_start = None
1057-
value = line[:comment_start].strip()
1058-
if not value:
1059-
if self._empty_lines_in_values:
1060-
# add empty line to the value, but only if there was no
1061-
# comment on the line
1062-
if (comment_start is None and
1063-
cursect is not None and
1064-
optname and
1065-
cursect[optname] is not None):
1066-
cursect[optname].append('') # newlines added at join
1067-
else:
1068-
# empty line marks end of value
1069-
indent_level = sys.maxsize
1070-
continue
1071-
# continuation line?
1072-
first_nonspace = self.NONSPACECRE.search(line)
1073-
cur_indent_level = first_nonspace.start() if first_nonspace else 0
1074-
if (cursect is not None and optname and
1075-
cur_indent_level > indent_level):
1076-
cursect[optname].append(value)
1077-
# a section header or option header?
1078-
else:
1079-
indent_level = cur_indent_level
1080-
# is it a section header?
1081-
mo = self.SECTCRE.match(value)
1082-
if mo:
1083-
sectname = mo.group('header')
1084-
if sectname in self._sections:
1085-
if self._strict and sectname in elements_added:
1086-
raise DuplicateSectionError(sectname, fpname,
1087-
lineno)
1088-
cursect = self._sections[sectname]
1089-
elements_added.add(sectname)
1090-
elif sectname == self.default_section:
1091-
cursect = self._defaults
1036+
try:
1037+
for lineno, line in enumerate(fp, start=1):
1038+
comment_start = sys.maxsize
1039+
# strip inline comments
1040+
inline_prefixes = {p: -1 for p in self._inline_comment_prefixes}
1041+
while comment_start == sys.maxsize and inline_prefixes:
1042+
next_prefixes = {}
1043+
for prefix, index in inline_prefixes.items():
1044+
index = line.find(prefix, index+1)
1045+
if index == -1:
1046+
continue
1047+
next_prefixes[prefix] = index
1048+
if index == 0 or (index > 0 and line[index-1].isspace()):
1049+
comment_start = min(comment_start, index)
1050+
inline_prefixes = next_prefixes
1051+
# strip full line comments
1052+
for prefix in self._comment_prefixes:
1053+
if line.strip().startswith(prefix):
1054+
comment_start = 0
1055+
break
1056+
if comment_start == sys.maxsize:
1057+
comment_start = None
1058+
value = line[:comment_start].strip()
1059+
if not value:
1060+
if self._empty_lines_in_values:
1061+
# add empty line to the value, but only if there was no
1062+
# comment on the line
1063+
if (comment_start is None and
1064+
cursect is not None and
1065+
optname and
1066+
cursect[optname] is not None):
1067+
cursect[optname].append('') # newlines added at join
10921068
else:
1093-
cursect = self._dict()
1094-
self._sections[sectname] = cursect
1095-
self._proxies[sectname] = SectionProxy(self, sectname)
1096-
elements_added.add(sectname)
1097-
# So sections can't start with a continuation line
1098-
optname = None
1099-
# no section header in the file?
1100-
elif cursect is None:
1101-
raise MissingSectionHeaderError(fpname, lineno, line)
1102-
# an option line?
1069+
# empty line marks end of value
1070+
indent_level = sys.maxsize
1071+
continue
1072+
# continuation line?
1073+
first_nonspace = self.NONSPACECRE.search(line)
1074+
cur_indent_level = first_nonspace.start() if first_nonspace else 0
1075+
if (cursect is not None and optname and
1076+
cur_indent_level > indent_level):
1077+
cursect[optname].append(value)
1078+
# a section header or option header?
11031079
else:
1104-
mo = self._optcre.match(value)
1080+
indent_level = cur_indent_level
1081+
# is it a section header?
1082+
mo = self.SECTCRE.match(value)
11051083
if mo:
1106-
optname, vi, optval = mo.group('option', 'vi', 'value')
1107-
if not optname:
1108-
e = self._handle_error(e, fpname, lineno, line)
1109-
optname = self.optionxform(optname.rstrip())
1110-
if (self._strict and
1111-
(sectname, optname) in elements_added):
1112-
raise DuplicateOptionError(sectname, optname,
1113-
fpname, lineno)
1114-
elements_added.add((sectname, optname))
1115-
# This check is fine because the OPTCRE cannot
1116-
# match if it would set optval to None
1117-
if optval is not None:
1118-
optval = optval.strip()
1119-
cursect[optname] = [optval]
1084+
sectname = mo.group('header')
1085+
if sectname in self._sections:
1086+
if self._strict and sectname in elements_added:
1087+
raise DuplicateSectionError(sectname, fpname,
1088+
lineno)
1089+
cursect = self._sections[sectname]
1090+
elements_added.add(sectname)
1091+
elif sectname == self.default_section:
1092+
cursect = self._defaults
11201093
else:
1121-
# valueless option handling
1122-
cursect[optname] = None
1094+
cursect = self._dict()
1095+
self._sections[sectname] = cursect
1096+
self._proxies[sectname] = SectionProxy(self, sectname)
1097+
elements_added.add(sectname)
1098+
# So sections can't start with a continuation line
1099+
optname = None
1100+
# no section header in the file?
1101+
elif cursect is None:
1102+
raise MissingSectionHeaderError(fpname, lineno, line)
1103+
# an option line?
11231104
else:
1124-
# a non-fatal parsing error occurred. set up the
1125-
# exception but keep going. the exception will be
1126-
# raised at the end of the file and will contain a
1127-
# list of all bogus lines
1128-
e = self._handle_error(e, fpname, lineno, line)
1129-
self._join_multiline_values()
1105+
mo = self._optcre.match(value)
1106+
if mo:
1107+
optname, vi, optval = mo.group('option', 'vi', 'value')
1108+
if not optname:
1109+
e = self._handle_error(e, fpname, lineno, line)
1110+
optname = self.optionxform(optname.rstrip())
1111+
if (self._strict and
1112+
(sectname, optname) in elements_added):
1113+
raise DuplicateOptionError(sectname, optname,
1114+
fpname, lineno)
1115+
elements_added.add((sectname, optname))
1116+
# This check is fine because the OPTCRE cannot
1117+
# match if it would set optval to None
1118+
if optval is not None:
1119+
optval = optval.strip()
1120+
cursect[optname] = [optval]
1121+
else:
1122+
# valueless option handling
1123+
cursect[optname] = None
1124+
else:
1125+
# a non-fatal parsing error occurred. set up the
1126+
# exception but keep going. the exception will be
1127+
# raised at the end of the file and will contain a
1128+
# list of all bogus lines
1129+
e = self._handle_error(e, fpname, lineno, line)
1130+
finally:
1131+
self._join_multiline_values()
11301132
# if any parsing errors occurred, raise an exception
11311133
if e:
11321134
raise e

Lib/test/test_configparser.py

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

650+
def test_get_after_duplicate_option_error(self):
651+
cf = self.newconfig()
652+
ini = textwrap.dedent("""\
653+
[Foo]
654+
x{equals}1
655+
y{equals}2
656+
y{equals}3
657+
""".format(equals=self.delimiters[0]))
658+
if self.strict:
659+
with self.assertRaises(configparser.DuplicateOptionError):
660+
cf.read_string(ini)
661+
else:
662+
cf.read_string(ini)
663+
self.assertEqual(cf.get('Foo', 'x'), '1')
664+
650665
def test_write(self):
651666
config_string = (
652667
"[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)