Skip to content

Commit b88569e

Browse files
committed
gh-157456: Fix configparser parsing an empty value with a whitespace-ending delimiter
An option whose delimiter ends in whitespace (e.g. ``delimiters=(' ',)``) and has an empty value could not be parsed, and the parser could not read back the output produced by ``write()``: ``_read`` matched option lines against ``line.clean``, which is ``str.strip``-ed on both sides, so for a whitespace delimiter the trailing whitespace that separates the key from the (empty) value was removed and ``OPTCRE`` no longer matched. Match the option regex against a form that preserves trailing whitespace. The matched value is stripped immediately afterwards, so ordinary values keep no trailing whitespace, and the other uses of ``line.clean`` are unchanged. Add a regression test; the gh-156353 whitespace-delimiter tests only used non-empty values, so this edge was unpinned.
1 parent fb46c67 commit b88569e

3 files changed

Lines changed: 31 additions & 3 deletions

File tree

Lib/configparser.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -599,8 +599,9 @@ def __init__(self, full_prefixes, inline_prefixes):
599599
)
600600
self.pattern = re.compile('|'.join(itertools.chain(full_patterns, inline_patterns)))
601601

602-
def strip(self, text):
603-
return self.pattern.sub('', text).rstrip()
602+
def strip(self, text, *, rstrip=True):
603+
text = self.pattern.sub('', text)
604+
return text.rstrip() if rstrip else text
604605

605606
def wrap(self, text):
606607
return _Line(text, self)
@@ -1156,7 +1157,13 @@ def _handle_option(self, st, line, fpname):
11561157
# an option line?
11571158
st.indent_level = st.cur_indent_level
11581159

1159-
mo = self._optcre.match(line.clean)
1160+
# `line.clean` strips trailing whitespace, but for a whitespace delimiter
1161+
# that whitespace is the separator before an (empty) value, so match
1162+
# against a form that preserves it. `optval` is stripped below, so
1163+
# ordinary values are unaffected.
1164+
match_target = self._comments.strip(str(line).strip('\r\n').lstrip(),
1165+
rstrip=False)
1166+
mo = self._optcre.match(match_target)
11601167
if not mo:
11611168
# a non-fatal parsing error occurred. set up the
11621169
# exception but keep going. the exception will be

Lib/test/test_configparser.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,23 @@ def test_any_delimiter(self, delimiter, space_before, space_after):
385385
self.assertEqual(cf.options('all'), ['foo'])
386386
self.assertEqual(cf.get('all', 'foo'), 'bar=baz')
387387

388+
def test_whitespace_delimiter_empty_value(self):
389+
# gh-157456: an option with a whitespace-ending delimiter and an empty
390+
# value must parse rather than raise ParsingError, and the parser must
391+
# read back the output produced by write().
392+
cf = self.newconfig(delimiters=(' ',))
393+
cf.read_string("[all]\nkey \n")
394+
# With allow_no_value a whitespace delimiter cannot distinguish an
395+
# empty value from a valueless option, so the value is None there;
396+
# otherwise it is the empty string.
397+
expected = None if cf._allow_no_value else ''
398+
self.assertEqual(cf.get('all', 'key'), expected)
399+
output = io.StringIO()
400+
cf.write(output)
401+
cf2 = self.newconfig(delimiters=(' ',))
402+
cf2.read_string(output.getvalue())
403+
self.assertEqual(cf2.get('all', 'key'), expected)
404+
388405
def test_basic_from_dict(self):
389406
config = {
390407
"Foo Bar": {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :mod:`configparser` failing to parse an option with an empty value when
2+
the delimiter ends in whitespace (for example ``delimiters=(' ',)``). Such an
3+
option, including the output produced by :meth:`~configparser.RawConfigParser.write`,
4+
is now read back correctly.

0 commit comments

Comments
 (0)