diff --git a/Doc/library/configparser.rst b/Doc/library/configparser.rst index 18e5bc20f3f690..445626c267fb6f 100644 --- a/Doc/library/configparser.rst +++ b/Doc/library/configparser.rst @@ -978,6 +978,10 @@ ConfigParser Objects The default *dict_type* is :class:`dict`, since it now preserves insertion order. + .. versionchanged:: 3.13 + Raise a :exc:`MultilineContinuationError` when *allow_no_value* is + ``True``, and a key without a value is continued with an indented line. + .. method:: defaults() Return a dictionary containing the instance-wide defaults. @@ -1349,6 +1353,13 @@ Exceptions The ``filename`` attribute and :meth:`!__init__` constructor argument were removed. They have been available using the name ``source`` since 3.2. +.. exception:: MultilineContinuationError + + Exception raised when a key without a corresponding value is continued with + an indented line. + + .. versionadded:: 3.13 + .. rubric:: Footnotes .. [1] Config parsers allow for heavy customization. If you are interested in diff --git a/Lib/configparser.py b/Lib/configparser.py index 71362d23ec3757..241f10aee93ec4 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -152,6 +152,7 @@ "NoOptionError", "InterpolationError", "InterpolationDepthError", "InterpolationMissingOptionError", "InterpolationSyntaxError", "ParsingError", "MissingSectionHeaderError", + "MultilineContinuationError", "ConfigParser", "RawConfigParser", "Interpolation", "BasicInterpolation", "ExtendedInterpolation", "SectionProxy", "ConverterMapping", @@ -322,6 +323,19 @@ def __init__(self, filename, lineno, line): self.args = (filename, lineno, line) +class MultilineContinuationError(ParsingError): + """Raised when a key without value is followed by continuation line""" + def __init__(self, filename, lineno, line): + Error.__init__( + self, + "Key without value continued with an indented line.\n" + "file: %r, line: %d\n%r" + %(filename, lineno, line)) + self.source = filename + self.lineno = lineno + self.line = line + self.args = (filename, lineno, line) + # Used in parser getters to indicate the default behaviour when a specific # option is not found it to raise an exception. Created to enable `None` as # a valid fallback value. @@ -987,6 +1001,8 @@ def _read(self, fp, fpname): cur_indent_level = first_nonspace.start() if first_nonspace else 0 if (cursect is not None and optname and cur_indent_level > indent_level): + if cursect[optname] is None: + raise MultilineContinuationError(fpname, lineno, line) cursect[optname].append(value) # a section header or option header? else: diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 2d7dfbde7082ee..5d58e34740adaf 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -1555,6 +1555,30 @@ def test_source_as_bytes(self): "'[badbad'" ) + def test_keys_without_value_with_extra_whitespace(self): + lines = [ + '[SECT]\n', + 'KEY1\n', + ' KEY2 = VAL2\n', # note the Space before the key! + ] + parser = configparser.ConfigParser( + comment_prefixes="", + allow_no_value=True, + strict=False, + delimiters=('=',), + interpolation=None, + ) + with self.assertRaises(configparser.MultilineContinuationError) as dse: + parser.read_file(lines) + self.assertEqual( + str(dse.exception), + "Key without value continued with an indented line.\n" + "file: '', line: 3\n" + "' KEY2 = VAL2\\n'" + ) + + + class CoverageOneHundredTestCase(unittest.TestCase): """Covers edge cases in the codebase.""" diff --git a/Misc/NEWS.d/next/Library/2023-08-05-08-41-58.gh-issue-107625.cVSHCT.rst b/Misc/NEWS.d/next/Library/2023-08-05-08-41-58.gh-issue-107625.cVSHCT.rst new file mode 100644 index 00000000000000..bf779c454e6388 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-08-05-08-41-58.gh-issue-107625.cVSHCT.rst @@ -0,0 +1,4 @@ +Raise :exc:`configparser.ParsingError` from :meth:`~configparser.ConfigParser.read` +and :meth:`~configparser.ConfigParser.read_file` methods of +:class:`configparser.ConfigParser` if a key without a corresponding value +is continued (that is, followed by an indented line).