Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion Lib/configparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,12 +923,20 @@ def popitem(self):
The section DEFAULT is never returned because it cannot be removed.
"""
for key in self.sections():
for key in self._sections:
value = self[key]
del self[key]
return key, value
raise KeyError

def clear(self):
"""Remove all sections from the parser.
The section DEFAULT is left intact because it cannot be removed.
"""
for section in self.sections():
del self[section]

def optionxform(self, optionstr):
return optionstr.lower()

Expand Down Expand Up @@ -1341,6 +1349,39 @@ def _options(self):
else:
return self._parser.defaults()

# MutableMapping.popitem() and MutableMapping.clear() create a new
# iterator for every removed option, and __iter__() copies the names
# of all options. Emptying a section that way takes quadratic time.

def popitem(self):
"""Remove an option from the section and return it as
an (option_name, value) tuple. If no option is present, raise KeyError.
Options of the section DEFAULT are only returned for that section
because they cannot be removed from other sections.
"""
if self._name != self._parser.default_section:
try:
options = self._parser._sections[self._name]
except KeyError:
raise NoSectionError(self._name) from None
else:
options = self._parser.defaults()
for key in options:
value = self[key]
del self[key]
return key, value
raise KeyError

def clear(self):
"""Remove all options from the section.
Options of the section DEFAULT are left intact for other sections
because they cannot be removed from them.
"""
for key in list(self._options()):
self._parser.remove_option(self._name, key)

@property
def parser(self):
# The parser object of the proxy is read-only.
Expand Down
78 changes: 78 additions & 0 deletions Lib/test/test_configparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,52 @@ def test_clear(self):
self.assertEqual(set(cf.sections()), set())
self.assertEqual(set(cf[self.default_section].keys()), {'foo'})

def test_section_popitem(self):
cf = self.fromstring(f"""
[section]
name1 {self.delimiters[0]} value1
name2 {self.delimiters[0]} value2
name3 {self.delimiters[0]} value3
""", defaults={"name2": "<default>", "name4": "<default>"})
section = cf['section']
self.assertEqual(section.popitem(), ('name1', 'value1'))
self.assertEqual(section.popitem(), ('name2', 'value2'))
self.assertEqual(section.popitem(), ('name3', 'value3'))
with self.assertRaises(KeyError):
section.popitem()
self.assertEqual(dict(section), {'name2': '<default>',
'name4': '<default>'})
default = cf[self.default_section]
self.assertEqual(default.popitem(), ('name2', '<default>'))
self.assertEqual(default.popitem(), ('name4', '<default>'))
with self.assertRaises(KeyError):
default.popitem()
self.assertEqual(dict(section), {})
del cf['section']
with self.assertRaises(configparser.NoSectionError):
section.popitem()

def test_section_clear(self):
cf = self.fromstring(f"""
[section1]
name1 {self.delimiters[0]} value1
name2 {self.delimiters[0]} value2
[section2]
name3 {self.delimiters[0]} value3
""", defaults={"name2": "<default>", "name4": "<default>"})
section = cf['section1']
section.clear()
self.assertEqual(dict(section), {'name2': '<default>',
'name4': '<default>'})
self.assertEqual(cf.sections(), ['section1', 'section2'])
cf[self.default_section].clear()
self.assertEqual(dict(cf[self.default_section]), {})
self.assertEqual(dict(section), {})
self.assertEqual(dict(cf['section2']), {'name3': 'value3'})
del cf['section1']
with self.assertRaises(configparser.NoSectionError):
section.clear()

def test_setitem(self):
cf = self.fromstring("""
[section1]
Expand Down Expand Up @@ -970,6 +1016,11 @@ def test_interpolation_missing_value(self):
self.assertEqual(e.args, ('name', 'Interpolation Error',
'%(reference)s', 'reference'))

def test_section_clear_does_not_interpolate(self):
cf = self.get_interpolation_config()
cf["Interpolation Error"].clear()
self.assertEqual(cf.options("Interpolation Error"), [])

def test_items(self):
self.check_items_config([('default', '<default>'),
('getdefault', '|<default>|'),
Expand Down Expand Up @@ -2341,6 +2392,33 @@ def test_option_regex_no_value_does_not_backtrack(self):
self.assertTrue(parser.has_option("section", "x" + " " * 40000 + "y"))


class MappingComplexityTestCase(unittest.TestCase):
"""Removing all sections or all options should take linear time."""

@support.requires_resource('cpu')
def test_clear_many_sections(self):
n = 50000
parser = configparser.RawConfigParser()
parser.read_string("".join(f"[{i}]\n" for i in range(n)))
self.assertEqual(len(parser.sections()), n)
# A quadratic clear() would copy the names of all remaining
# sections for every removed section.
parser.clear()
self.assertEqual(parser.sections(), [])

@support.requires_resource('cpu')
def test_clear_many_options(self):
n = 50000
parser = configparser.RawConfigParser({"key": "value"})
parser["section"] = {str(i): str(i) for i in range(n)}
section = parser["section"]
self.assertEqual(len(section), n + 1)
# A quadratic clear() would copy the whole section merged with
# the defaults for every removed option.
section.clear()
self.assertEqual(dict(section), {"key": "value"})


class MiscTestCase(unittest.TestCase):
def test__all__(self):
support.check__all__(self, configparser, not_exported={"Error"})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix quadratic time complexity of :meth:`!clear` and :meth:`!popitem` on
:class:`configparser.ConfigParser` objects and their section proxies.
Clearing a section no longer fails part way with
:exc:`~configparser.InterpolationError` on a value it removes.
Loading