From 10a5a062fee33821a9194452ef99af42dd7482a1 Mon Sep 17 00:00:00 2001 From: Pedro Sousa Lacerda Date: Wed, 21 Sep 2022 11:18:21 -0300 Subject: [PATCH 1/7] Introducing UNNAMED_SECTION --- Lib/configparser.py | 41 +++++++++++++++++++++++++----- Lib/test/test_configparser.py | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index f98e6fb01f97cc..b8d2d946b9182f 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 must be appropriate for %()s string interpolation. @@ -67,6 +68,10 @@ 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 these is + ``configparser.UNNAMED_SECTION``. + sections() Return all the configuration section names, sans DEFAULT. @@ -155,7 +160,7 @@ "ConfigParser", "RawConfigParser", "Interpolation", "BasicInterpolation", "ExtendedInterpolation", "LegacyInterpolation", "SectionProxy", "ConverterMapping", - "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH") + "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION") _default_dict = dict DEFAULTSECT = "DEFAULT" @@ -322,6 +327,15 @@ def __init__(self, filename, lineno, line): self.args = (filename, lineno, line) +class _UnnamedSection: + + def __repr__(self): + return "" + + +UNNAMED_SECTION = _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. @@ -583,7 +597,8 @@ def __init__(self, defaults=None, dict_type=_default_dict, comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=DEFAULTSECT, - interpolation=_UNSET, converters=_UNSET): + interpolation=_UNSET, converters=_UNSET, + allow_unnamed_section=False,): self._dict = dict_type self._sections = self._dict() @@ -622,6 +637,7 @@ def __init__(self, defaults=None, dict_type=_default_dict, self._converters.update(converters) if defaults: self._read_defaults(defaults) + self._allow_unnamed_section = allow_unnamed_section def defaults(self): return self._defaults @@ -895,13 +911,19 @@ def write(self, fp, space_around_delimiters=True): if self._defaults: self._write_section(fp, self.default_section, self._defaults.items(), d) + if UNNAMED_SECTION in self._sections: + self._write_section(fp, UNNAMED_SECTION, self._sections[UNNAMED_SECTION].items(), d, unnamed=True) + for section in self._sections: + if section is UNNAMED_SECTION: + continue self._write_section(fp, section, self._sections[section].items(), d) - def _write_section(self, fp, section_name, section_items, delimiter): + def _write_section(self, fp, section_name, section_items, delimiter, unnamed=False): """Write a single section to the specified `fp'.""" - fp.write("[{}]\n".format(section_name)) + if not unnamed: + fp.write("[{}]\n".format(section_name)) for key, value in section_items: value = self._interpolation.before_write(self, section_name, key, value) @@ -1037,6 +1059,13 @@ def _read(self, fp, fpname): cursect[optname].append(value) # a section header or option header? else: + if self._allow_unnamed_section and cursect is None: + sectname = UNNAMED_SECTION + cursect = self._dict() + self._sections[sectname] = cursect + self._proxies[sectname] = SectionProxy(self, sectname) + elements_added.add(sectname) + indent_level = cur_indent_level # is it a section header? mo = self.SECTCRE.match(value) @@ -1057,7 +1086,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? diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index da17c00063c56d..6c79dbd44a0c78 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2119,6 +2119,54 @@ def test_instance_assignment(self): self.assertEqual(cfg['two'].getlen('one'), 5) +class SectionlessTestCase(unittest.TestCase): + + def fromstring(self, string): + cfg = configparser.ConfigParser(allow_unnamed_section=True) + cfg.read_string(string) + return cfg + + def test_no_first_section(self): + cfg1 = self.fromstring(""" + a = 1 + b = 2 + [sect1] + c = 3 + """) + + self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), set(cfg1.sections())) + self.assertEqual('1', cfg1[configparser.UNNAMED_SECTION]['a']) + self.assertEqual('2', cfg1[configparser.UNNAMED_SECTION]['b']) + self.assertEqual('3', cfg1['sect1']['c']) + + output = io.StringIO() + cfg1.write(output) + cfg2 = self.fromstring(output.getvalue()) + + #self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), set(cfg2.sections())) + self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a']) + self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b']) + self.assertEqual('3', cfg2['sect1']['c']) + + def test_no_section(self): + cfg1 = self.fromstring(""" + a = 1 + b = 2 + """) + + self.assertEqual([configparser.UNNAMED_SECTION], cfg1.sections()) + self.assertEqual('1', cfg1[configparser.UNNAMED_SECTION]['a']) + self.assertEqual('2', cfg1[configparser.UNNAMED_SECTION]['b']) + + output = io.StringIO() + cfg1.write(output) + cfg2 = self.fromstring(output.getvalue()) + + self.assertEqual([configparser.UNNAMED_SECTION], cfg2.sections()) + self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a']) + self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b']) + + class MiscTestCase(unittest.TestCase): def test__all__(self): support.check__all__(self, configparser, not_exported={"Error"}) From 72be2b1dd55953ae202e60d2f51e6debd96d7ca3 Mon Sep 17 00:00:00 2001 From: Pedro Sousa Lacerda Date: Thu, 22 Sep 2022 07:10:04 -0300 Subject: [PATCH 2/7] Accepting changes --- Lib/configparser.py | 15 +++++++++------ Lib/test/test_configparser.py | 6 ++++-- .../2022-09-22-07-33-00.bpo-22253.jdo4573.rst | 4 ++++ 3 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-09-22-07-33-00.bpo-22253.jdo4573.rst diff --git a/Lib/configparser.py b/Lib/configparser.py index b8d2d946b9182f..bccb31974266b0 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -69,8 +69,10 @@ section proxies. When `allow_unnamed_section` is True (default: False), options - without section are accepted: the section for these is - ``configparser.UNNAMED_SECTION``. + 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. @@ -598,7 +600,7 @@ def __init__(self, defaults=None, dict_type=_default_dict, strict=True, empty_lines_in_values=True, default_section=DEFAULTSECT, interpolation=_UNSET, converters=_UNSET, - allow_unnamed_section=False,): + allow_unnamed_section=False): self._dict = dict_type self._sections = self._dict() @@ -912,7 +914,8 @@ def write(self, fp, space_around_delimiters=True): self._write_section(fp, self.default_section, self._defaults.items(), d) if UNNAMED_SECTION in self._sections: - self._write_section(fp, UNNAMED_SECTION, self._sections[UNNAMED_SECTION].items(), d, unnamed=True) + self._write_section(fp, UNNAMED_SECTION, + self._sections[UNNAMED_SECTION].items(), d) for section in self._sections: if section is UNNAMED_SECTION: @@ -920,9 +923,9 @@ def write(self, fp, space_around_delimiters=True): self._write_section(fp, section, self._sections[section].items(), d) - def _write_section(self, fp, section_name, section_items, delimiter, unnamed=False): + def _write_section(self, fp, section_name, section_items, delimiter): """Write a single section to the specified `fp'.""" - if not unnamed: + if section_name is not UNNAMED_SECTION: fp.write("[{}]\n".format(section_name)) for key, value in section_items: value = self._interpolation.before_write(self, section_name, key, diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 6c79dbd44a0c78..26c33146aa731c 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2134,7 +2134,8 @@ def test_no_first_section(self): c = 3 """) - self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), set(cfg1.sections())) + self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), + set(cfg1.sections())) self.assertEqual('1', cfg1[configparser.UNNAMED_SECTION]['a']) self.assertEqual('2', cfg1[configparser.UNNAMED_SECTION]['b']) self.assertEqual('3', cfg1['sect1']['c']) @@ -2143,7 +2144,8 @@ def test_no_first_section(self): cfg1.write(output) cfg2 = self.fromstring(output.getvalue()) - #self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), set(cfg2.sections())) + self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), + set(cfg2.sections())) self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a']) self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b']) self.assertEqual('3', cfg2['sect1']['c']) 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..e068238109da6f --- /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. + +Contributed by Pedro Sousa Lacerda. From 8b7764f31e855afc6b610b827d73b72d56670533 Mon Sep 17 00:00:00 2001 From: Pedro Sousa Lacerda Date: Fri, 23 Sep 2022 09:21:49 -0300 Subject: [PATCH 3/7] Accepting changes --- Lib/configparser.py | 9 ++++++++- Lib/test/test_configparser.py | 19 +++++++++++++++++++ .../2022-09-22-07-33-00.bpo-22253.jdo4573.rst | 4 ++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index bccb31974266b0..0fb34d8fe7d7a9 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -70,7 +70,7 @@ 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 + ``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. @@ -1119,6 +1119,13 @@ 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) + + # delete UNNAMED_SECTION if it's empty + if (UNNAMED_SECTION in self._sections and not + self._sections[UNNAMED_SECTION]): + del self._sections[UNNAMED_SECTION] + del self._proxies[UNNAMED_SECTION] + 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 26c33146aa731c..93d5c692d0c9b1 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2168,6 +2168,25 @@ def test_no_section(self): self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a']) self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b']) + def test_one_named_section(self): + cfg1 = self.fromstring(""" + [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(output.getvalue()) + + self.assertEqual(['sect1'], cfg2.sections()) + self.assertEqual('1', cfg2['sect1']['a']) + self.assertEqual('2', cfg2['sect1']['b']) + class MiscTestCase(unittest.TestCase): def test__all__(self): 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 index e068238109da6f..c3a336c33d2383 100644 --- 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 @@ -1,4 +1,4 @@ :class:`configparser.ConfigParser` now accepts unnamed sections before named -ones. +ones, if configured to do so. -Contributed by Pedro Sousa Lacerda. +Contributed by Pedro Sousa Lacerda and Christian Siefkes. From 7a05656e891d2583c3661028b2ddd08ac69084bb Mon Sep 17 00:00:00 2001 From: Pedro Sousa Lacerda Date: Mon, 3 Oct 2022 11:23:39 -0300 Subject: [PATCH 4/7] Write DEFAULT section before unnamed --- Lib/configparser.py | 7 +++---- Lib/test/test_configparser.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index 0fb34d8fe7d7a9..bed67af2793c41 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -910,13 +910,12 @@ def write(self, fp, space_around_delimiters=True): d = " {} ".format(self._delimiters[0]) else: d = self._delimiters[0] - if self._defaults: - self._write_section(fp, self.default_section, - self._defaults.items(), d) if UNNAMED_SECTION in self._sections: self._write_section(fp, UNNAMED_SECTION, self._sections[UNNAMED_SECTION].items(), d) - + if self._defaults: + self._write_section(fp, self.default_section, + self._defaults.items(), d) for section in self._sections: if section is UNNAMED_SECTION: continue diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 93d5c692d0c9b1..fa2246ce82b8fc 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2187,6 +2187,36 @@ def test_one_named_section(self): self.assertEqual('1', cfg2['sect1']['a']) self.assertEqual('2', cfg2['sect1']['b']) + def test_unnamed_and_default_section(self): + cfg1 = self.fromstring(""" + a = 1 + b = 2 + [DEFAULT] + d = 4 + [sect1] + c = 3 + """) + + self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), + set(cfg1.sections())) + self.assertEqual('1', cfg1[configparser.UNNAMED_SECTION]['a']) + self.assertEqual('2', cfg1[configparser.UNNAMED_SECTION]['b']) + self.assertEqual('4', cfg1[configparser.UNNAMED_SECTION]['d']) + self.assertEqual('3', cfg1['sect1']['c']) + self.assertEqual('4', cfg1['sect1']['d']) + + output = io.StringIO() + cfg1.write(output) + cfg2 = self.fromstring(output.getvalue()) + + self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), + set(cfg2.sections())) + self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a']) + self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b']) + self.assertEqual('4', cfg2[configparser.UNNAMED_SECTION]['d']) + self.assertEqual('3', cfg2['sect1']['c']) + self.assertEqual('4', cfg2['sect1']['d']) + class MiscTestCase(unittest.TestCase): def test__all__(self): From d7b6281001df9774c5852826f520c4baa462b94a Mon Sep 17 00:00:00 2001 From: Pedro Sousa Lacerda Date: Wed, 5 Oct 2022 10:49:19 -0300 Subject: [PATCH 5/7] Turn unnamed section a default section --- Lib/configparser.py | 37 ++++++----------- Lib/test/test_configparser.py | 77 ++++++++++++----------------------- 2 files changed, 38 insertions(+), 76 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index bed67af2793c41..35488f7be2626f 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -162,7 +162,7 @@ "ConfigParser", "RawConfigParser", "Interpolation", "BasicInterpolation", "ExtendedInterpolation", "LegacyInterpolation", "SectionProxy", "ConverterMapping", - "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION") + "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "TOP_LEVEL") _default_dict = dict DEFAULTSECT = "DEFAULT" @@ -332,10 +332,15 @@ def __init__(self, filename, lineno, line): class _UnnamedSection: def __repr__(self): - return "" + return "" + def __eq__(self, other): + return repr(self) == repr(other) -UNNAMED_SECTION = _UnnamedSection() + def __hash__(self): + return hash(repr(self)) + +TOP_LEVEL = _UnnamedSection() # Used in parser getters to indicate the default behaviour when a specific @@ -599,8 +604,7 @@ def __init__(self, defaults=None, dict_type=_default_dict, comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=DEFAULTSECT, - interpolation=_UNSET, converters=_UNSET, - allow_unnamed_section=False): + interpolation=_UNSET, converters=_UNSET): self._dict = dict_type self._sections = self._dict() @@ -639,7 +643,6 @@ def __init__(self, defaults=None, dict_type=_default_dict, self._converters.update(converters) if defaults: self._read_defaults(defaults) - self._allow_unnamed_section = allow_unnamed_section def defaults(self): return self._defaults @@ -910,21 +913,16 @@ def write(self, fp, space_around_delimiters=True): d = " {} ".format(self._delimiters[0]) else: d = self._delimiters[0] - if UNNAMED_SECTION in self._sections: - self._write_section(fp, UNNAMED_SECTION, - self._sections[UNNAMED_SECTION].items(), d) if self._defaults: self._write_section(fp, self.default_section, self._defaults.items(), d) for section in self._sections: - if section is UNNAMED_SECTION: - continue self._write_section(fp, section, self._sections[section].items(), d) def _write_section(self, fp, section_name, section_items, delimiter): """Write a single section to the specified `fp'.""" - if section_name is not UNNAMED_SECTION: + if section_name is not TOP_LEVEL: fp.write("[{}]\n".format(section_name)) for key, value in section_items: value = self._interpolation.before_write(self, section_name, key, @@ -1061,12 +1059,9 @@ def _read(self, fp, fpname): cursect[optname].append(value) # a section header or option header? else: - if self._allow_unnamed_section and cursect is None: - sectname = UNNAMED_SECTION - cursect = self._dict() - self._sections[sectname] = cursect - self._proxies[sectname] = SectionProxy(self, sectname) - elements_added.add(sectname) + 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? @@ -1119,12 +1114,6 @@ def _read(self, fp, fpname): # list of all bogus lines e = self._handle_error(e, fpname, lineno, line) - # delete UNNAMED_SECTION if it's empty - if (UNNAMED_SECTION in self._sections and not - self._sections[UNNAMED_SECTION]): - del self._sections[UNNAMED_SECTION] - del self._proxies[UNNAMED_SECTION] - 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 fa2246ce82b8fc..52c4b5bd7217f2 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2121,55 +2121,31 @@ def test_instance_assignment(self): class SectionlessTestCase(unittest.TestCase): - def fromstring(self, string): - cfg = configparser.ConfigParser(allow_unnamed_section=True) + def fromstring(self, default_section, string): + cfg = configparser.ConfigParser(default_section=default_section) cfg.read_string(string) return cfg - def test_no_first_section(self): - cfg1 = self.fromstring(""" - a = 1 - b = 2 - [sect1] - c = 3 - """) - - self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), - set(cfg1.sections())) - self.assertEqual('1', cfg1[configparser.UNNAMED_SECTION]['a']) - self.assertEqual('2', cfg1[configparser.UNNAMED_SECTION]['b']) - self.assertEqual('3', cfg1['sect1']['c']) - - output = io.StringIO() - cfg1.write(output) - cfg2 = self.fromstring(output.getvalue()) - - self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), - set(cfg2.sections())) - self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a']) - self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b']) - self.assertEqual('3', cfg2['sect1']['c']) - - def test_no_section(self): - cfg1 = self.fromstring(""" + def test_only_top_level(self): + cfg1 = self.fromstring(configparser.TOP_LEVEL, """ a = 1 b = 2 """) - self.assertEqual([configparser.UNNAMED_SECTION], cfg1.sections()) - self.assertEqual('1', cfg1[configparser.UNNAMED_SECTION]['a']) - self.assertEqual('2', cfg1[configparser.UNNAMED_SECTION]['b']) + 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(output.getvalue()) - self.assertEqual([configparser.UNNAMED_SECTION], cfg2.sections()) - self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a']) - self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b']) + self.assertEqual([], cfg1.sections()) + self.assertEqual('1', cfg2[configparser.TOP_LEVEL]['a']) + self.assertEqual('2', cfg2[configparser.TOP_LEVEL]['b']) - def test_one_named_section(self): - cfg1 = self.fromstring(""" + def test_only_regular(self): + cfg1 = self.fromstring(configparser.TOP_LEVEL, """ [sect1] a = 1 b = 2 @@ -2187,35 +2163,32 @@ def test_one_named_section(self): self.assertEqual('1', cfg2['sect1']['a']) self.assertEqual('2', cfg2['sect1']['b']) - def test_unnamed_and_default_section(self): - cfg1 = self.fromstring(""" + def test_top_level_and_regular(self): + cfg1 = self.fromstring(configparser.TOP_LEVEL, """ a = 1 b = 2 - [DEFAULT] - d = 4 [sect1] c = 3 + d = %(a)s%(b)s%(c)s """) - self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), - set(cfg1.sections())) - self.assertEqual('1', cfg1[configparser.UNNAMED_SECTION]['a']) - self.assertEqual('2', cfg1[configparser.UNNAMED_SECTION]['b']) - self.assertEqual('4', cfg1[configparser.UNNAMED_SECTION]['d']) + 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('4', cfg1['sect1']['d']) + self.assertEqual('1', cfg1['sect1']['a']) + self.assertEqual('123', cfg1['sect1']['d']) output = io.StringIO() cfg1.write(output) cfg2 = self.fromstring(output.getvalue()) - self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), - set(cfg2.sections())) - self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a']) - self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b']) - self.assertEqual('4', cfg2[configparser.UNNAMED_SECTION]['d']) + 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('4', cfg2['sect1']['d']) + self.assertEqual('1', cfg2['sect1']['a']) + self.assertEqual('123', cfg2['sect1']['d']) class MiscTestCase(unittest.TestCase): From 1e0ceb199e4b0803a0da8a6be883d290ea8f1861 Mon Sep 17 00:00:00 2001 From: Pedro Sousa Lacerda Date: Wed, 5 Oct 2022 10:54:02 -0300 Subject: [PATCH 6/7] Minor fix --- Lib/test/test_configparser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 52c4b5bd7217f2..6c3bb85dbb5f7f 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2138,7 +2138,7 @@ def test_only_top_level(self): output = io.StringIO() cfg1.write(output) - cfg2 = self.fromstring(output.getvalue()) + cfg2 = self.fromstring(configparser.TOP_LEVEL, output.getvalue()) self.assertEqual([], cfg1.sections()) self.assertEqual('1', cfg2[configparser.TOP_LEVEL]['a']) @@ -2157,7 +2157,7 @@ def test_only_regular(self): output = io.StringIO() cfg1.write(output) - cfg2 = self.fromstring(output.getvalue()) + cfg2 = self.fromstring(configparser.TOP_LEVEL, output.getvalue()) self.assertEqual(['sect1'], cfg2.sections()) self.assertEqual('1', cfg2['sect1']['a']) @@ -2181,7 +2181,7 @@ def test_top_level_and_regular(self): output = io.StringIO() cfg1.write(output) - cfg2 = self.fromstring(output.getvalue()) + cfg2 = self.fromstring(configparser.TOP_LEVEL, output.getvalue()) self.assertEqual(['sect1'], cfg1.sections()) self.assertEqual('1', cfg2[configparser.TOP_LEVEL]['a']) From fdb70812f12e0d65c8c81b79b801f989943b3eaa Mon Sep 17 00:00:00 2001 From: Pedro Lacerda Date: Fri, 29 Mar 2024 11:32:01 -0300 Subject: [PATCH 7/7] Update Lib/configparser.py Co-authored-by: Furkan Onder --- Lib/configparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index aee47544b5bffc..0546a9bf8e491c 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -924,7 +924,7 @@ 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`.""" if section_name is not TOP_LEVEL: - fp.write("[{}]\n".format(section_name)) + fp.write(f"[{section_name}]\n") for key, value in section_items: value = self._interpolation.before_write(self, section_name, key, value)