Skip to content

gh-123049: Allow to create the unnamed section from scratch. #123077

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 18, 2024
20 changes: 16 additions & 4 deletions Doc/library/configparser.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1314,13 +1314,19 @@ RawConfigParser Objects

.. method:: add_section(section)

Add a section named *section* to the instance. If a section by the given
name already exists, :exc:`DuplicateSectionError` is raised. If the
*default section* name is passed, :exc:`ValueError` is raised.
Add a section named *section* or :const:`UNNAMED_SECTION` to the instance.

If the given section already exists, :exc:`DuplicateSectionError` is
raised. If the *default section* name is passed, :exc:`ValueError` is
raised. If :const:`UNNAMED_SECTION` is passed and support is disabled,
:exc:`UnnamedSectionDisabledError` is raised.

Type of *section* is not checked which lets users create non-string named
sections. This behaviour is unsupported and may cause internal errors.

.. versionchanged:: 3.14
Added support for :const:`UNNAMED_SECTION`.


.. method:: set(section, option, value)

Expand Down Expand Up @@ -1405,7 +1411,6 @@ Exceptions
Exception raised when attempting to parse a file which has no section
headers.


.. exception:: ParsingError

Exception raised when errors occur attempting to parse a file.
Expand All @@ -1421,6 +1426,13 @@ Exceptions

.. versionadded:: 3.13

.. exception:: UnnamedSectionDisabledError

Exception raised when attempting to use the
:const:`UNNAMED_SECTION` without enabling it.

.. versionadded:: 3.14

.. rubric:: Footnotes

.. [1] Config parsers allow for heavy customization. If you are interested in
Expand Down
30 changes: 21 additions & 9 deletions Lib/configparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
"NoOptionError", "InterpolationError", "InterpolationDepthError",
"InterpolationMissingOptionError", "InterpolationSyntaxError",
"ParsingError", "MissingSectionHeaderError",
"MultilineContinuationError",
"MultilineContinuationError", "UnnamedSectionDisabledError",
"ConfigParser", "RawConfigParser",
"Interpolation", "BasicInterpolation", "ExtendedInterpolation",
"SectionProxy", "ConverterMapping",
Expand Down Expand Up @@ -362,6 +362,14 @@ def __init__(self, filename, lineno, line):
self.line = line
self.args = (filename, lineno, line)


class UnnamedSectionDisabledError(Error):
"""Raised when an attempt to use UNNAMED_SECTION is made with the
feature disabled."""
def __init__(self):
Error.__init__(self, "Support for UNNAMED_SECTION is disabled.")


class _UnnamedSection:

def __repr__(self):
Expand Down Expand Up @@ -692,6 +700,10 @@ def add_section(self, section):
if section == self.default_section:
raise ValueError('Invalid section name: %r' % section)

if section is UNNAMED_SECTION:
if not self._allow_unnamed_section:
raise UnnamedSectionDisabledError

if section in self._sections:
raise DuplicateSectionError(section)
self._sections[section] = self._dict()
Expand Down Expand Up @@ -1203,20 +1215,20 @@ def _convert_to_boolean(self, value):
return self.BOOLEAN_STATES[value.lower()]

def _validate_value_types(self, *, section="", option="", value=""):
"""Raises a TypeError for non-string values.
"""Raises a TypeError for illegal non-string values.

The only legal non-string value if we allow valueless
options is None, so we need to check if the value is a
string if:
- we do not allow valueless options, or
- we allow valueless options but the value is not None
Legal non-string values are UNNAMED_SECTION and falsey values if
they are allowed.

For compatibility reasons this method is not used in classic set()
for RawConfigParsers. It is invoked in every case for mapping protocol
access and in ConfigParser.set().
"""
if not isinstance(section, str):
raise TypeError("section names must be strings")
if section is UNNAMED_SECTION:
if not self._allow_unnamed_section:
raise UnnamedSectionDisabledError
elif not isinstance(section, str):
raise TypeError("section names must be strings or UNNAMED_SECTION")
if not isinstance(option, str):
raise TypeError("option keys must be strings")
if not self._allow_no_value or value:
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_configparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2161,6 +2161,19 @@ def test_no_section(self):
self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a'])
self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b'])

def test_add_section(self):
cfg = configparser.ConfigParser(allow_unnamed_section=True)
cfg.add_section(configparser.UNNAMED_SECTION)
cfg.set(configparser.UNNAMED_SECTION, 'a', '1')
self.assertEqual('1', cfg[configparser.UNNAMED_SECTION]['a'])

def test_disabled_error(self):
with self.assertRaises(configparser.MissingSectionHeaderError):
configparser.ConfigParser().read_string("a = 1")

with self.assertRaises(configparser.UnnamedSectionDisabledError):
configparser.ConfigParser().add_section(configparser.UNNAMED_SECTION)


class MiscTestCase(unittest.TestCase):
def test__all__(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add support for :const:`~configparser.UNNAMED_SECTION`
in :meth:`configparser.ConfigParser.add_section`.
Loading