8000 [3.12] gh-116957: configparser: Do post-process values after Duplicat… · python/cpython@0fc8ae4 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0fc8ae4

Browse files
authored
[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 b1bc375)
1 parent 688623d commit 0fc8ae4

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
@@ -995,100 +995,102 @@ def _read(self, fp, fpname):
995995
lineno = 0
996996
indent_level = 0
997997
e = None # None, or an exception
998-
for lineno, line in enumerate(fp, start=1):
999-
comment_start = sys.maxsize
1000-
# strip inline comments
1001-
inline_prefixes = {p: -1 for p in self._inline_comment_prefixes}
1002-
while comment_start == sys.maxsize and inline_prefixes:
1003-
next_prefixes = {}
1004-
for prefix, index in inline_prefixes.items():
1005-
index = line.find(prefix, index+1)
1006-
if index == -1:
1007-
continue
1008-
next_prefixes[prefix] = index
1009-
if index == 0 or (index > 0 and line[index-1].isspace()):
1010-
comment_start = min(comment_start, index)
1011-
inline_prefixes = next_prefixes
1012-
# strip full line comments
1013-
for prefix in self._comment_prefixes:
1014-
if line.strip().startswith(prefix):
1015-
comment_start = 0
1016-
break
1017-
if comment_start == sys.maxsize:
1018-
comment_start = None
1019-
value = line[:comment_start].strip()
1020-
if not value:
1021-
if self._empty_lines_in_values:
1022-
# add empty line to the value, but only if there was no
1023-
# comment on the line
1024-
if (comment_start is None and
1025-
cursect is not None and
1026-
optname and
1027-
cursect[optname] is not None):
1028-
cursect[optname].append('') # newlines added at join
1029-
else:
1030-
# empty line marks end of value
1031-
indent_level = sys.maxsize
1032-
continue
1033-
# continuation line?
1034-
first_nonspace = self.NONSPACECRE.search(line)
1035-
cur_indent_level = first_nonspace.start() if first_nonspace else 0
1036-
if (cursect is not None and optname and
1037-
cur_indent_level > indent_level):
1038-
cursect[optname].append(value)
1039-
# a section header or option header?
1040-
else:
1041-
indent_level = cur_indent_level
1042-
# is it a section header?
1043-
mo = self.SECTCRE.match(value)
1044-
if mo:
1045-
sectname = mo.group('header')
1046-
if sectname in self._sections:
1047-
if self._strict and sectname in elements_added:
1048-
raise DuplicateSectionError(sectname, fpname,
1049-
lineno)
1050-
cursect = self._sections[sectname]
1051-
elements_added.add(sectname)
1052-
elif sectname == self.default_section:
1053-
cursect = self._defaults
998+
try:
999+
for lineno, line in enumerate(fp, start=1):
1000+
comment_start = sys.maxsize
1001+
# strip inline comments
1002+
inline_prefixes = {p: -1 for p in self._inline_comment_prefixes}
1003+
while comment_start == sys.maxsize and inline_prefixes:
1004+
next_prefixes = {}
1005+
for prefix, index in inline_prefixes.items():
1006+
index = line.find(prefix, index+1)
1007+
if index == -1:
1008+
continue
1009+
next_prefixes[prefix] = index
1010+
if index == 0 or (index > 0 and line[index-1].isspace()):
1011+
comment_start = min(comment_start, index)
1012+
inline_prefixes = next_prefixes
1013+
# strip full line comments
1014+
for prefix in self._comment_prefixes:
1015+
if line.strip().startswith(prefix):
1016+
comment_start = 0
1017+
break
1018+
if comment_start == sys.maxsize:
1019+
comment_start = None
1020+
value = line[:comment_start].strip()
1021+
if not value:
1022+
if self._empty_lines_in_values:
1023+
# add empty line to the value, but only if there was no
1024+
# comment on the line
1025+
if (comment_start is None and
1026+
cursect is not None and
1027+
optname and
1028+
cursect[optname] is not None):
1029+
cursect[optname].append('') # newlines added at join
10541030
else:
1055-
cursect = self._dict()
1056-
self._sections[sectname] = cursect
1057-
self._proxies[sectname] = SectionProxy(self, sectname)
1058-
elements_added.add(sectname)
1059-
# So sections can't start with a continuation line
1060-
optname = None
1061-
# no section header in the file?
1062-
elif cursect is None:
1063-
raise MissingSectionHeaderError(fpname, lineno, line)
1064-
# an option line?
1031+
# empty line marks end of value
1032+
indent_level = sys.maxsize
1033+
continue
1034+
# continuation line?
1035+
first_nonspace = self.NONSPACECRE.search(line)
1036+
cur_indent_level = first_nonspace.start() if first_nonspace else 0
1037+
if (cursect is not None and optname and
1038+
cur_indent_level > indent_level):
1039+
cursect[optname].append(value)
1040+
# a section header or option header?
10651041
else:
1066-
mo = self._optcre.match(value)
1042+
indent_level = cur_indent_level
1043+
# is it a section header?
1044+
mo = self.SECTCRE.match(value)
10671045
if mo:
1068-
optname, vi, optval = mo.group('option', 'vi', 'value')
1069-
if not optname:
1070-
e = self._handle_error(e, fpname, lineno, line)
1071-
optname = self.optionxform(optname.rstrip())
1072-
if (self._strict and
1073-
(sectname, optname) in elements_added):
1074-
raise DuplicateOptionError(sectname, optname,
1075-
fpname, lineno)
1076-
elements_added.add((sectname, optname))
1077-
# This check is fine because the OPTCRE cannot
1078-
# match if it would set optval to None
1079-
if optval is not None:
1080-
optval = optval.strip()
1081-
cursect[optname] = [optval]
1046+
sectname = mo.group('header')
1047+
if sectname in self._sections:
1048+
if self._strict and sectname in elements_added:
1049+
raise DuplicateSectionError(sectname, fpname,
1050+
lineno)
1051+
cursect = self._sections[sectname]
1052+
elements_added.add(sectname)
1053+
elif sectname == self.default_section:
1054+
cursect = self._defaults
10821055
else:
1083-
# valueless option handling
1084-
cursect[optname] = None
1056+
cursect = self._dict()
1057+
self._sections[sectname] = cursect
1058+
self._proxies[sectname] = SectionProxy(self, sectname)
1059+
elements_added.add(sectname)
1060+
# So sections can't start with a continuation line
1061+
optname = None
1062+
# no section header in the file?
1063+
elif cursect is None:
1064+
raise MissingSectionHeaderError(fpname, lineno, line)
1065+
# an option line?
10851066
else:
1086-
# a non-fatal parsing error occurred. set up the
1087-
# exception but keep going. the exception will be
1088-
# raised at the end of the file and will contain a
1089-
# list of all bogus lines
1090-
e = self._handle_error(e, fpname, lineno, line)
1091-
self._join_multiline_values()
1067+
mo = self._optcre.match(value)
1068+
if mo:
1069+
optname, vi, optval = mo.group('option', 'vi', 'value')
1070+
if not optname:
1071+
e = self._handle_error(e, fpname, lineno, line)
1072+
optname = self.optionxform(optname.rstrip())
1073+
if (self._strict and
1074+
(sectname, optname) in elements_added):
1075+
raise DuplicateOptionError(sectname, optname,
1076+
fpname, lineno)
1077+
elements_added.add((sectname, optname))
1078+
# This check is fine because the OPTCRE cannot
1079+
# match if it would set optval to None
1080+
if optval is not None:
1081+
optval = optval.strip()
1082+
cursect[optname] = [optval]
1083+
else:
1084+
# valueless option handling
1085+
cursect[optname] = None
1086+
else:
1087+
# a non-fatal parsing error occurred. set up the
1088+
# exception but keep going. the exception will be
1089+
# raised at the end of the file and will contain a
1090+
# list of all bogus lines
1091+
e = self._handle_error(e, fpname, lineno, line)
1092+
finally:
1093+
self._join_multiline_values()
10921094
# if any parsing errors occurred, raise an exception
10931095
if e:
10941096
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)
0