8000 gh-66449: configparser: Add support for unnamed sections (#117273) · python/cpython@54f7e14 · GitHub
[go: up one dir, main page]

Skip to content

Commit 54f7e14

Browse files
pslacerdajaraco
andauthored
gh-66449: configparser: Add support for unnamed sections (#117273)
Co-authored-by: Jason R. Coombs <jaraco@jaraco.com>
1 parent d9cfe7e commit 54f7e14

File tree

5 files changed

+172
-31
lines changed

5 files changed

+172
-31
lines changed

Doc/library/configparser.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,11 @@ may be treated as parts of multiline values or ignored.
274274
By default, a valid section name can be any string that does not contain '\\n'.
275275
To change this, see :attr:`ConfigParser.SECTCRE`.
276276

277+
The first section name may be omitted if the parser is configured to allow an
278+
unnamed top level section with ``allow_unnamed_section=True``. In this case,
279+
the keys/values may be retrieved by :const:`UNNAMED_SECTION` as in
280+
``config[UNNAMED_SECTION]``.
281+
277282
Configuration files may include comments, prefixed by specific
278283
characters (``#`` and ``;`` by default [1]_). Comments may appear on
279284
their own on an otherwise empty line, possibly indented. [1]_
@@ -325,6 +330,27 @@ For example:
325330
# Did I mention we can indent comments, too?
326331
327332
333+
.. _unnamed-sections:
334+
335+
Unnamed Sections
336+
----------------
337+
338+
The name of the first section (or unique) may be omitted and values
339+
retrieved by the :const:`UNNAMED_SECTION` attribute.
340+
341+
.. doctest::
342+
343+
>>> config = """
344+
... option = value
345+
...
346+
... [ Section 2 ]
347+
... another = val
348+
... """
349+
>>> unnamed = configparser.ConfigParser(allow_unnamed_section=True)
350+
>>> unnamed.read_string(config)
351+
>>> unnamed.get(configparser.UNNAMED_SECTION, 'option')
352+
'value'
353+
328354
Interpolation of values
329355
-----------------------
330356

@@ -1216,6 +1242,11 @@ ConfigParser Objects
12161242
names is stripped before :meth:`optionxform` is called.
12171243

12181244

1245+
.. data:: UNNAMED_SECTION
1246+
1247+
A special object representing a section name used to reference the unnamed section (see :ref:`unnamed-sections`).
1248+
1249+
12191250
.. data:: MAX_INTERPOLATION_DEPTH
12201251

12211252
The maximum depth for recursive interpolation for :meth:`~configparser.ConfigParser.get` when the *raw*

Doc/whatsnew/3.13.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ Other Language Changes
214214

215215
(Contributed by William Woodruff in :gh:`112389`.)
216216

217+
* The :class:`configparser.ConfigParser` now accepts unnamed sections before named
218+
ones if configured to do so.
219+
220+
(Contributed by Pedro Sousa Lacerda in :gh:`66449`)
221+
222+
217223
New Modules
218224
===========
219225

Lib/configparser.py

Lines changed: 85 additions & 31 deletions
< 10000 tr class="diff-line-row">
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
delimiters=('=', ':'), co F438 mment_prefixes=('#', ';'),
1919
inline_comment_prefixes=None, strict=True,
2020
empty_lines_in_values=True, default_section='DEFAULT',
21-
interpolation=<unset>, converters=<unset>):
22-
21+
interpolation=<unset>, converters=<unset>,
22+
allow_unnamed_section=False):
2323
Create the parser. When `defaults` is given, it is initialized into the
2424
dictionary or intrinsic defaults. The keys must be strings, the values
2525
must be appropriate for %()s string interpolation.
@@ -68,6 +68,10 @@
6868
converter gets its corresponding get*() method on the parser object and
6969
section proxies.
7070
71+
When `allow_unnamed_section` is True (default: False), options
72+
without section are accepted: the section for these is
73+
``configparser.UNNAMED_SECTION``.
74+
7175
sections()
7276
Return all the configuration section names, sans DEFAULT.
7377
@@ -156,7 +160,7 @@
156160
"ConfigParser", "RawConfigParser",
157161
"Interpolation", "BasicInterpolation", "ExtendedInterpolation",
158162
"SectionProxy", "ConverterMapping",
159-
"DEFAULTSECT", "MAX_INTERPOLATION_DEPTH")
163+
"DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION")
160164

161165
_default_dict = dict
162166
DEFAULTSECT = "DEFAULT"
@@ -336,6 +340,15 @@ def __init__(self, filename, lineno, line):
336340
self.line = line
337341
self.args = (filename, lineno, line)
338342

343+
class _UnnamedSection:
344+
345+
def __repr__(self):
346+
return "<UNNAMED_SECTION>"
347+
348+
349+
UNNAMED_SECTION = _UnnamedSection()
350+
351+
339352
# Used in parser getters to indicate the default behaviour when a specific
340353
# option is not found it to raise an exception. Created to enable `None` as
341354
# a valid fallback value.
@@ -550,7 +563,8 @@ def __init__(self, defaults=None, dict_type=_default_dict,
550563
comment_prefixes=('#', ';'), inline_comment_prefixes=None,
551564
strict=True, empty_lines_in_values=True,
552565
default_section=DEFAULTSECT,
553-
interpolation=_UNSET, converters=_UNSET):
566+
interpolation=_UNSET, converters=_UNSET,
567+
allow_unnamed_section=False,):
554568

555569
self._dict = dict_type
556570
self._sections = self._dict()
@@ -589,6 +603,7 @@ def __init__(self, defaults=None, dict_type=_default_dict,
589603
self._converters.update(converters)
590604
if defaults:
591605
self._read_defaults(defaults)
606+
self._allow_unnamed_section = allow_unnamed_section
592607

593608
def defaults(self):
594609
return self._defaults
@@ -862,13 +877,19 @@ def write(self, fp, space_around_delimiters=True):
862877
if self._defaults:
863878
self._write_section(fp, self.default_section,
864879
self._defaults.items(), d)
880+
if UNNAMED_SECTION in self._sections:
881+
self._write_section(fp, UNNAMED_SECTION, self._sections[UNNAMED_SECTION].items(), d, unnamed=True)
882+
865883
for section in self._sections:
884+
if section is UNNAMED_SECTION:
885+
continue
866886
self._write_section(fp, section,
867887
self._sections[section].items(), d)
868888

869-
def _write_section(self, fp, section_name, section_items, delimiter):
870-
"""Write a single section to the specified `fp`."""
871-
fp.write("[{}]\n".format(section_name))
889+
def _write_section(self, fp, section_name, section_items, delimiter, unnamed=False):
890+
"""Write a single section to the specified `fp'."""
891+
if not unnamed:
892+
fp.write("[{}]\n".format(section_name))
872893
for key, value in section_items:
873894
value = self._interpolation.before_write(self, section_name, key,
874895
value)
@@ -961,6 +982,7 @@ def _read(self, fp, fpname):
961982
lineno = 0
962983
indent_level = 0
963984
e = None # None, or an exception
985+
964986
try:
965987
for lineno, line in enumerate(fp, start=1):
966988
comment_start = sys.maxsize
@@ -1007,6 +1029,13 @@ def _read(self, fp, fpname):
10071029
cursect[optname].append(value)
10081030
# a section header or option header?
10091031
else:
1032+
if self._allow_unnamed_section and cursect is None:
1033+
sectname = UNNAMED_SECTION
1034+
cursect = self._dict()
1035+
self._sections[sectname] = cursect
1036+
self._proxies[sectname] = SectionProxy(self, sectname)
1037+
elements_added.add(sectname)
1038+
10101039
indent_level = cur_indent_level
10111040
# is it a section header?
10121041
mo = self.SECTCRE.match(value)
@@ -1027,36 +1056,61 @@ def _read(self, fp, fpname):
10271056
elements_added.add(sectname)
10281057
# So sections can't start with a continuation line
10291058
optname = None
1030-
# no section header in the file?
1059+
# no section header?
10311060
elif cursect is None:
10321061
raise MissingSectionHeaderError(fpname, lineno, line)
1033-
# an option line?
1062+
# an option line?
10341063
else:
1035-
mo = self._optcre.match(value)
1064+
indent_level = cur_indent_level
1065+
# is it a section header?
1066+
mo = self.SECTCRE.match(value)
10361067
if mo:
1037-
optname, vi, optval = mo.group('option', 'vi', 'value')
1038-
if not optname:
1039-
e = self._handle_error(e, fpname, lineno, line)
1040-
optname = self.optionxform(optname.rstrip())
1041-
if (self._strict and
1042-
(sectname, optname) in elements_added):
1043-
raise DuplicateOptionError(sectname, optname,
1044-
fpname, lineno)
1045-
elements_added.add((sectname, optname))
1046-
# This check is fine because the OPTCRE cannot
1047-
# match if it would set optval to None
1048-
if optval is not None:
1049-
optval = optval.strip()
1050-
cursect[optname] = [optval]
1068+
sectname = mo.group('header')
1069+
if sectname in self._sections:
1070+
if self._strict and sectname in elements_added:
1071+
raise DuplicateSectionError(sectname, fpname,
1072+
lineno)
1073+
cursect = self._sections[sectname]
1074+
elements_added.add(sectname)
1075+
elif sectname == self.default_section:
1076+
cursect = self._defaults
10511077
else:
1052-
# valueless option handling
1053-
cursect[optname] = None
1078+
cursect = self._dict()
1079+
self._sections[sectname] = cursect
1080+
self._proxies[sectname] = SectionProxy(self, sectname)
1081+
elements_added.add(sectname)
1082+
# So sections can't start with a continuation line
1083+
optname = None
1084+
# no section header in the file?
1085+
elif cursect is None:
1086+
raise MissingSectionHeaderError(fpname, lineno, line)
1087+
# an option line?
10541088
else:
1055-
# a non-fatal parsing error occurred. set up the
1056-
# exception but keep going. the exception will be
1057-
# raised at the end of the file and will contain a
1058-
# list of all bogus lines
1059-
e = self._handle_error(e, fpname, lineno, line)
1089+
mo = self._optcre.match(value)
1090+
if mo:
1091+
optname, vi, optval = mo.group('option', 'vi', 'value')
1092+
if not optname:
1093+
e = self._handle_error(e, fpname, lineno, line)
1094+
optname = self.optionxform(optname.rstrip())
1095+
if (self._strict and
1096+
(sectname, optname) in elements_added):
1097+
raise DuplicateOptionError(sectname, optname,
1098+
fpname, lineno)
1099+
elements_added.add((sectname, optname))
1100+
# This check is fine because the OPTCRE cannot
1101+
# match if it would set optval to None
1102+
if optval is not None:
1103+
optval = optval.strip()
1104+
cursect[optname] = [optval]
1105+
else:
1106+
# valueless option handling
1107+
cursect[optname] = None
1108+
else:
1109+
# a non-fatal parsing error occurred. set up the
1110+
# exception but keep going. the exception will be
1111+
# raised at the end of the file and will contain a
1112+
# list of all bogus lines
1113+
e = self._handle_error(e, fpname, lineno, line)
10601114
finally:
10611115
self._join_multiline_values()
10621116
# if any parsing errors occurred, raise an exception

Lib/test/test_configparser.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,6 +2115,54 @@ def test_instance_assignment(self):
21152115
self.assertEqual(cfg['two'].getlen('one'), 5)
21162116

21172117

2118+
class SectionlessTestCase(unittest.TestCase):
2119+
2120+
def fromstring(self, string):
2121+
cfg = configparser.ConfigParser(allow_unnamed_section=True)
2122+
cfg.read_string(string)
2123+
return cfg
2124+
2125+
def test_no_first_section(self):
2126+
cfg1 = self.fromstring("""
2127+
a = 1
2128+
b = 2
2129+
[sect1]
2130+
c = 3
2131+
""")
2132+
2133+
self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), set(cfg1.sections()))
2134+
self.assertEqual('1', cfg1[configparser.UNNAMED_SECTION]['a'])
2135+
self.assertEqual('2', cfg1[configparser.UNNAMED_SECTION]['b'])
2136+
self.assertEqual('3', cfg1['sect1']['c'])
2137+
2138+
output = io.StringIO()
2139+
cfg1.write(output)
2140+
cfg2 = self.fromstring(output.getvalue())
2141+
2142+
#self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), set(cfg2.sections()))
2143+
self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a'])
2144+
self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b'])
2145+
self.assertEqual('3', cfg2['sect1']['c'])
2146+
2147+
def test_no_section(self):
2148+
cfg1 = self.fromstring("""
2149+
a = 1
2150+
b = 2
2151+
""")
2152+
2153+
self.assertEqual([configparser.UNNAMED_SECTION], cfg1.sections())
2154+
self.assertEqual('1', cfg1[configparser.UNNAMED_SECTION]['a'])
2155+
self.assertEqual('2', cfg1[configparser.UNNAMED_SECTION]['b'])
2156+
2157+
output = io.StringIO()
2158+
cfg1.write(output)
2159+
cfg2 = self.fromstring(output.getvalue())
2160+
2161+
self.assertEqual([configparser.UNNAMED_SECTION], cfg2.sections())
2162+
self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a'])
2163+
self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b'])
2164+
2165+
21182166
class MiscTestCase(unittest.TestCase):
21192167
def test__all__(self):
21202168
support.check__all__(self, configparser, not_exported={"Error"})
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:class:`configparser.ConfigParser` now accepts unnamed sections before named
2+
ones, if configured to do so.

0 commit comments

Comments
 (0)
0