diff --git a/Lib/configparser.py b/Lib/configparser.py index dee5a0db7e7ddc..0546a9bf8e491c 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -18,7 +18,8 @@ delimiters=('=', ':'), comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section='DEFAULT', - interpolation=, converters=): + interpolation=, converters=, + allow_unnamed_section=False): Create the parser. When `defaults` is given, it is initialized into the dictionary or intrinsic defaults. The keys must be strings, the values @@ -68,6 +69,12 @@ converter gets its corresponding get*() method on the parser object and section proxies. + When `allow_unnamed_section` is True (default: False), options + without section are accepted: the section for this is + ``configparser.UNNAMED_SECTION``. In the file, they are placed before + the first explicit section header. This also allows reading and writing + files that don't contain any explicit section headers. + sections() Return all the configuration section names, sans DEFAULT. @@ -156,7 +163,7 @@ "ConfigParser", "RawConfigParser", "Interpolation", "BasicInterpolation", "ExtendedInterpolation", "LegacyInterpolation", "SectionProxy", "ConverterMapping", - "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH") + "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "TOP_LEVEL") _default_dict = dict DEFAULTSECT = "DEFAULT" @@ -323,6 +330,20 @@ def __init__(self, filename, lineno, line): self.args = (filename, lineno, line) +class _UnnamedSection: + + def __repr__(self): + return "" + + def __eq__(self, other): + return repr(self) == repr(other) + + def __hash__(self): + return hash(repr(self)) + +TOP_LEVEL = _UnnamedSection() + + # 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. @@ -902,7 +923,8 @@ def write(self, fp, space_around_delimiters=True): def _write_section(self, fp, section_name, section_items, delimiter): """Write a single section to the specified `fp`.""" - fp.write("[{}]\n".format(section_name)) + if section_name is not TOP_LEVEL: + fp.write(f"[{section_name}]\n") for key, value in section_items: value = self._interpolation.before_write(self, section_name, key, value) @@ -1038,6 +1060,10 @@ def _read(self, fp, fpname): cursect[optname].append(value) # a section header or option header? else: + if self.default_section is TOP_LEVEL and cursect is None: + sectname = TOP_LEVEL + cursect = self._defaults + indent_level = cur_indent_level # is it a section header? mo = self.SECTCRE.match(value) @@ -1058,7 +1084,7 @@ def _read(self, fp, fpname): elements_added.add(sectname) # So sections can't start with a continuation line optname = None - # no section header in the file? + # no section header? elif cursect is None: raise MissingSectionHeaderError(fpname, lineno, line) # an option line? @@ -1088,6 +1114,7 @@ def _read(self, fp, fpname): # raised at the end of the file and will contain a # list of all bogus lines e = self._handle_error(e, fpname, lineno, line) + self._join_multiline_values() # if any parsing errors occurred, raise an exception if e: diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index da17c00063c56d..6c3bb85dbb5f7f 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2119,6 +2119,78 @@ def test_instance_assignment(self): self.assertEqual(cfg['two'].getlen('one'), 5) +class SectionlessTestCase(unittest.TestCase): + + def fromstring(self, default_section, string): + cfg = configparser.ConfigParser(default_section=default_section) + cfg.read_string(string) + return cfg + + def test_only_top_level(self): + cfg1 = self.fromstring(configparser.TOP_LEVEL, """ + a = 1 + b = 2 + """) + + self.assertEqual([], cfg1.sections()) + self.assertEqual('1', cfg1[configparser.TOP_LEVEL]['a']) + self.assertEqual('2', cfg1[configparser.TOP_LEVEL]['b']) + + output = io.StringIO() + cfg1.write(output) + cfg2 = self.fromstring(configparser.TOP_LEVEL, output.getvalue()) + + self.assertEqual([], cfg1.sections()) + self.assertEqual('1', cfg2[configparser.TOP_LEVEL]['a']) + self.assertEqual('2', cfg2[configparser.TOP_LEVEL]['b']) + + def test_only_regular(self): + cfg1 = self.fromstring(configparser.TOP_LEVEL, """ + [sect1] + a = 1 + b = 2 + """) + + self.assertEqual(['sect1'], cfg1.sections()) + self.assertEqual('1', cfg1['sect1']['a']) + self.assertEqual('2', cfg1['sect1']['b']) + + output = io.StringIO() + cfg1.write(output) + cfg2 = self.fromstring(configparser.TOP_LEVEL, output.getvalue()) + + self.assertEqual(['sect1'], cfg2.sections()) + self.assertEqual('1', cfg2['sect1']['a']) + self.assertEqual('2', cfg2['sect1']['b']) + + def test_top_level_and_regular(self): + cfg1 = self.fromstring(configparser.TOP_LEVEL, """ + a = 1 + b = 2 + [sect1] + c = 3 + d = %(a)s%(b)s%(c)s + """) + + self.assertEqual(['sect1'], cfg1.sections()) + self.assertEqual('1', cfg1[configparser.TOP_LEVEL]['a']) + self.assertEqual('2', cfg1[configparser.TOP_LEVEL]['b']) + self.assertEqual('3', cfg1['sect1']['c']) + self.assertEqual('1', cfg1['sect1']['a']) + self.assertEqual('123', cfg1['sect1']['d']) + + output = io.StringIO() + cfg1.write(output) + cfg2 = self.fromstring(configparser.TOP_LEVEL, output.getvalue()) + + self.assertEqual(['sect1'], cfg1.sections()) + self.assertEqual('1', cfg2[configparser.TOP_LEVEL]['a']) + self.assertEqual('2', cfg2[configparser.TOP_LEVEL]['b']) + self.assertEqual('3', cfg2['sect1']['c']) + self.assertEqual('1', cfg2['sect1']['a']) + self.assertEqual('123', cfg2['sect1']['d']) + + class MiscTestCase(unittest.TestCase): def test__all__(self): support.check__all__(self, configparser, not_exported={"Error"}) diff --git a/Misc/NEWS.d/next/Library/2022-09-22-07-33-00.bpo-22253.jdo4573.rst b/Misc/NEWS.d/next/Library/2022-09-22-07-33-00.bpo-22253.jdo4573.rst new file mode 100644 index 00000000000000..c3a336c33d2383 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-09-22-07-33-00.bpo-22253.jdo4573.rst @@ -0,0 +1,4 @@ +:class:`configparser.ConfigParser` now accepts unnamed sections before named +ones, if configured to do so. + +Contributed by Pedro Sousa Lacerda and Christian Siefkes.