summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPedro Lacerda <pslacerda@users.noreply.github.com>2024-03-29 15:05:00 (GMT)
committerGitHub <noreply@github.com>2024-03-29 15:05:00 (GMT)
commit54f7e14500471d1c46fb553adb3ca24cd1fef084 (patch)
tree2e6156ee3443d338af5a5016ccdb31c8e7c6260d
parentd9cfe7e565a6e2dc15747a904736264e31a10be4 (diff)
downloadcpython-54f7e14500471d1c46fb553adb3ca24cd1fef084.zip
cpython-54f7e14500471d1c46fb553adb3ca24cd1fef084.tar.gz
cpython-54f7e14500471d1c46fb553adb3ca24cd1fef084.tar.bz2
gh-66449: configparser: Add support for unnamed sections (#117273)
Co-authored-by: Jason R. Coombs <jaraco@jaraco.com>
-rw-r--r--Doc/library/configparser.rst31
-rw-r--r--Doc/whatsnew/3.13.rst6
-rw-r--r--Lib/configparser.py116
-rw-r--r--Lib/test/test_configparser.py48
-rw-r--r--Misc/NEWS.d/next/Library/2024-03-28-17-55-22.gh-issue-66449.4jhuEV.rst2
5 files changed, 172 insertions, 31 deletions
diff --git a/Doc/library/configparser.rst b/Doc/library/configparser.rst
index 445626c..9e7638d 100644
--- a/Doc/library/configparser.rst
+++ b/Doc/library/configparser.rst
@@ -274,6 +274,11 @@ may be treated as parts of multiline values or ignored.
By default, a valid section name can be any string that does not contain '\\n'.
To change this, see :attr:`ConfigParser.SECTCRE`.
+The first section name may be omitted if the parser is configured to allow an
+unnamed top level section with ``allow_unnamed_section=True``. In this case,
+the keys/values may be retrieved by :const:`UNNAMED_SECTION` as in
+``config[UNNAMED_SECTION]``.
+
Configuration files may include comments, prefixed by specific
characters (``#`` and ``;`` by default [1]_). Comments may appear on
their own on an otherwise empty line, possibly indented. [1]_
@@ -325,6 +330,27 @@ For example:
# Did I mention we can indent comments, too?
+.. _unnamed-sections:
+
+Unnamed Sections
+----------------
+
+The name of the first section (or unique) may be omitted and values
+retrieved by the :const:`UNNAMED_SECTION` attribute.
+
+.. doctest::
+
+ >>> config = """
+ ... option = value
+ ...
+ ... [ Section 2 ]
+ ... another = val
+ ... """
+ >>> unnamed = configparser.ConfigParser(allow_unnamed_section=True)
+ >>> unnamed.read_string(config)
+ >>> unnamed.get(configparser.UNNAMED_SECTION, 'option')
+ 'value'
+
Interpolation of values
-----------------------
@@ -1216,6 +1242,11 @@ ConfigParser Objects
names is stripped before :meth:`optionxform` is called.
+.. data:: UNNAMED_SECTION
+
+ A special object representing a section name used to reference the unnamed section (see :ref:`unnamed-sections`).
+
+
.. data:: MAX_INTERPOLATION_DEPTH
The maximum depth for recursive interpolation for :meth:`~configparser.ConfigParser.get` when the *raw*
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 5a5c506..f50364a 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -214,6 +214,12 @@ Other Language Changes
(Contributed by William Woodruff in :gh:`112389`.)
+* The :class:`configparser.ConfigParser` now accepts unnamed sections before named
+ ones if configured to do so.
+
+ (Contributed by Pedro Sousa Lacerda in :gh:`66449`)
+
+
New Modules
===========
diff --git a/Lib/configparser.py b/Lib/configparser.py
index 8f182ee..3040e1f 100644
--- a/Lib/configparser.py
+++ b/Lib/configparser.py
@@ -18,8 +18,8 @@ ConfigParser -- responsible for parsing a list of
delimiters=('=', ':'), comment_prefixes=('#', ';'),
inline_comment_prefixes=None, strict=True,
empty_lines_in_values=True, default_section='DEFAULT',
- interpolation=<unset>, converters=<unset>):
-
+ interpolation=<unset>, converters=<unset>,
+ 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.
@@ -68,6 +68,10 @@ ConfigParser -- responsible for parsing a list of
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.
@@ -156,7 +160,7 @@ __all__ = ("NoSectionError", "DuplicateOptionError", "DuplicateSectionError",
"ConfigParser", "RawConfigParser",
"Interpolation", "BasicInterpolation", "ExtendedInterpolation",
"SectionProxy", "ConverterMapping",
- "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH")
+ "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION")
_default_dict = dict
DEFAULTSECT = "DEFAULT"
@@ -336,6 +340,15 @@ class MultilineContinuationError(ParsingError):
self.line = line
self.args = (filename, lineno, line)
+class _UnnamedSection:
+
+ def __repr__(self):
+ return "<UNNAMED_SECTION>"
+
+
+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.
@@ -550,7 +563,8 @@ class RawConfigParser(MutableMapping):
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()
@@ -589,6 +603,7 @@ class RawConfigParser(MutableMapping):
self._converters.update(converters)
if defaults:
self._read_defaults(defaults)
+ self._allow_unnamed_section = allow_unnamed_section
def defaults(self):
return self._defaults
@@ -862,13 +877,19 @@ class RawConfigParser(MutableMapping):
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):
- """Write a single section to the specified `fp`."""
- fp.write("[{}]\n".format(section_name))
+ def _write_section(self, fp, section_name, section_items, delimiter, unnamed=False):
+ """Write a single section to the specified `fp'."""
+ 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)
@@ -961,6 +982,7 @@ class RawConfigParser(MutableMapping):
lineno = 0
indent_level = 0
e = None # None, or an exception
+
try:
for lineno, line in enumerate(fp, start=1):
comment_start = sys.maxsize
@@ -1007,6 +1029,13 @@ class RawConfigParser(MutableMapping):
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)
@@ -1027,36 +1056,61 @@ class RawConfigParser(MutableMapping):
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?
+ # an option line?
else:
- mo = self._optcre.match(value)
+ indent_level = cur_indent_level
+ # is it a section header?
+ mo = self.SECTCRE.match(value)
if mo:
- optname, vi, optval = mo.group('option', 'vi', 'value')
- if not optname:
- e = self._handle_error(e, fpname, lineno, line)
- optname = self.optionxform(optname.rstrip())
- if (self._strict and
- (sectname, optname) in elements_added):
- raise DuplicateOptionError(sectname, optname,
- fpname, lineno)
- elements_added.add((sectname, optname))
- # This check is fine because the OPTCRE cannot
- # match if it would set optval to None
- if optval is not None:
- optval = optval.strip()
- cursect[optname] = [optval]
+ sectname = mo.group('header')
+ if sectname in self._sections:
+ if self._strict and sectname in elements_added:
+ raise DuplicateSectionError(sectname, fpname,
+ lineno)
+ cursect = self._sections[sectname]
+ elements_added.add(sectname)
+ elif sectname == self.default_section:
+ cursect = self._defaults
else:
- # valueless option handling
- cursect[optname] = None
+ cursect = self._dict()
+ self._sections[sectname] = cursect
+ self._proxies[sectname] = SectionProxy(self, sectname)
+ elements_added.add(sectname)
+ # So sections can't start with a continuation line
+ optname = None
+ # no section header in the file?
+ elif cursect is None:
+ raise MissingSectionHeaderError(fpname, lineno, line)
+ # an option line?
else:
- # a non-fatal parsing error occurred. set up the
- # exception but keep going. the exception will be
- # raised at the end of the file and will contain a
- # list of all bogus lines
- e = self._handle_error(e, fpname, lineno, line)
+ mo = self._optcre.match(value)
+ if mo:
+ optname, vi, optval = mo.group('option', 'vi', 'value')
+ if not optname:
+ e = self._handle_error(e, fpname, lineno, line)
+ optname = self.optionxform(optname.rstrip())
+ if (self._strict and
+ (sectname, optname) in elements_added):
+ raise DuplicateOptionError(sectname, optname,
+ fpname, lineno)
+ elements_added.add((sectname, optname))
+ # This check is fine because the OPTCRE cannot
+ # match if it would set optval to None
+ if optval is not None:
+ optval = optval.strip()
+ cursect[optname] = [optval]
+ else:
+ # valueless option handling
+ cursect[optname] = None
+ else:
+ # a non-fatal parsing error occurred. set up the
+ # exception but keep going. the exception will be
+ # raised at the end of the file and will contain a
+ # list of all bogus lines
+ e = self._handle_error(e, fpname, lineno, line)
finally:
self._join_multiline_values()
# if any parsing errors occurred, raise an exception
diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py
index 6340e37..fe09472 100644
--- a/Lib/test/test_configparser.py
+++ b/Lib/test/test_configparser.py
@@ -2115,6 +2115,54 @@ class BlatantOverrideConvertersTestCase(unittest.TestCase):
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"})
diff --git a/Misc/NEWS.d/next/Library/2024-03-28-17-55-22.gh-issue-66449.4jhuEV.rst b/Misc/NEWS.d/next/Library/2024-03-28-17-55-22.gh-issue-66449.4jhuEV.rst
new file mode 100644
index 0000000..898100b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-03-28-17-55-22.gh-issue-66449.4jhuEV.rst
@@ -0,0 +1,2 @@
+:class:`configparser.ConfigParser` now accepts unnamed sections before named
+ones, if configured to do so.