mirror of https://github.com/python/cpython
gh-66449: configparser: Add support for unnamed sections (#117273)
Co-authored-by: Jason R. Coombs <jaraco@jaraco.com>
This commit is contained in:
parent
d9cfe7e565
commit
54f7e14500
|
@ -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*
|
||||
|
|
|
@ -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
|
||||
===========
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
:class:`configparser.ConfigParser` now accepts unnamed sections before named
|
||||
ones, if configured to do so.
|
Loading…
Reference in New Issue