Skip to content

Commit 89fe8f2

Browse files
authored
[3.15] gh-161108: Make configparser clear() linear time (GH-161109) (#161134)
Give SectionProxy its own popitem() and clear() and RawConfigParser its own clear() instead of the MutableMapping ones, which copied every remaining key for each removal. #161109 (cherry picked from commit 2a1282b)
1 parent 1ccb123 commit 89fe8f2

3 files changed

Lines changed: 124 additions & 1 deletion

File tree

‎Lib/configparser.py‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -923,12 +923,20 @@ def popitem(self):
923923
924924
The section DEFAULT is never returned because it cannot be removed.
925925
"""
926-
for key in self.sections():
926+
for key in self._sections:
927927
value = self[key]
928928
del self[key]
929929
return key, value
930930
raise KeyError
931931

932+
def clear(self):
933+
"""Remove all sections from the parser.
934+
935+
The section DEFAULT is left intact because it cannot be removed.
936+
"""
937+
for section in self.sections():
938+
del self[section]
939+
932940
def optionxform(self, optionstr):
933941
return optionstr.lower()
934942

@@ -1341,6 +1349,39 @@ def _options(self):
13411349
else:
13421350
return self._parser.defaults()
13431351

1352+
# MutableMapping.popitem() and MutableMapping.clear() create a new
1353+
# iterator for every removed option, and __iter__() copies the names
1354+
# of all options. Emptying a section that way takes quadratic time.
1355+
1356+
def popitem(self):
1357+
"""Remove an option from the section and return it as
1358+
an (option_name, value) tuple. If no option is present, raise KeyError.
1359+
1360+
Options of the section DEFAULT are only returned for that section
1361+
because they cannot be removed from other sections.
1362+
"""
1363+
if self._name != self._parser.default_section:
1364+
try:
1365+
options = self._parser._sections[self._name]
1366+
except KeyError:
1367+
raise NoSectionError(self._name) from None
1368+
else:
1369+
options = self._parser.defaults()
1370+
for key in options:
1371+
value = self[key]
1372+
del self[key]
1373+
return key, value
1374+
raise KeyError
1375+
1376+
def clear(self):
1377+
"""Remove all options from the section.
1378+
1379+
Options of the section DEFAULT are left intact for other sections
1380+
because they cannot be removed from them.
1381+
"""
1382+
for key in list(self._options()):
1383+
self._parser.remove_option(self._name, key)
1384+
13441385
@property
13451386
def parser(self):
13461387
# The parser object of the proxy is read-only.

‎Lib/test/test_configparser.py‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,52 @@ def test_clear(self):
884884
self.assertEqual(set(cf.sections()), set())
885885
self.assertEqual(set(cf[self.default_section].keys()), {'foo'})
886886

887+
def test_section_popitem(self):
888+
cf = self.fromstring(f"""
889+
[section]
890+
name1 {self.delimiters[0]} value1
891+
name2 {self.delimiters[0]} value2
892+
name3 {self.delimiters[0]} value3
893+
""", defaults={"name2": "<default>", "name4": "<default>"})
894+
section = cf['section']
895+
self.assertEqual(section.popitem(), ('name1', 'value1'))
896+
self.assertEqual(section.popitem(), ('name2', 'value2'))
897+
self.assertEqual(section.popitem(), ('name3', 'value3'))
898+
with self.assertRaises(KeyError):
899+
section.popitem()
900+
self.assertEqual(dict(section), {'name2': '<default>',
901+
'name4': '<default>'})
902+
default = cf[self.default_section]
903+
self.assertEqual(default.popitem(), ('name2', '<default>'))
904+
self.assertEqual(default.popitem(), ('name4', '<default>'))
905+
with self.assertRaises(KeyError):
906+
default.popitem()
907+
self.assertEqual(dict(section), {})
908+
del cf['section']
909+
with self.assertRaises(configparser.NoSectionError):
910+
section.popitem()
911+
912+
def test_section_clear(self):
913+
cf = self.fromstring(f"""
914+
[section1]
915+
name1 {self.delimiters[0]} value1
916+
name2 {self.delimiters[0]} value2
917+
[section2]
918+
name3 {self.delimiters[0]} value3
919+
""", defaults={"name2": "<default>", "name4": "<default>"})
920+
section = cf['section1']
921+
section.clear()
922+
self.assertEqual(dict(section), {'name2': '<default>',
923+
'name4': '<default>'})
924+
self.assertEqual(cf.sections(), ['section1', 'section2'])
925+
cf[self.default_section].clear()
926+
self.assertEqual(dict(cf[self.default_section]), {})
927+
self.assertEqual(dict(section), {})
928+
self.assertEqual(dict(cf['section2']), {'name3': 'value3'})
929+
del cf['section1']
930+
with self.assertRaises(configparser.NoSectionError):
931+
section.clear()
932+
887933
def test_setitem(self):
888934
cf = self.fromstring("""
889935
[section1]
@@ -970,6 +1016,11 @@ def test_interpolation_missing_value(self):
9701016
self.assertEqual(e.args, ('name', 'Interpolation Error',
9711017
'%(reference)s', 'reference'))
9721018

1019+
def test_section_clear_does_not_interpolate(self):
1020+
cf = self.get_interpolation_config()
1021+
cf["Interpolation Error"].clear()
1022+
self.assertEqual(cf.options("Interpolation Error"), [])
1023+
9731024
def test_items(self):
9741025
self.check_items_config([('default', '<default>'),
9751026
('getdefault', '|<default>|'),
@@ -2341,6 +2392,33 @@ def test_option_regex_no_value_does_not_backtrack(self):
23412392
self.assertTrue(parser.has_option("section", "x" + " " * 40000 + "y"))
23422393

23432394

2395+
class MappingComplexityTestCase(unittest.TestCase):
2396+
"""Removing all sections or all options should take linear time."""
2397+
2398+
@support.requires_resource('cpu')
2399+
def test_clear_many_sections(self):
2400+
n = 50000
2401+
parser = configparser.RawConfigParser()
2402+
parser.read_string("".join(f"[{i}]\n" for i in range(n)))
2403+
self.assertEqual(len(parser.sections()), n)
2404+
# A quadratic clear() would copy the names of all remaining
2405+
# sections for every removed section.
2406+
parser.clear()
2407+
self.assertEqual(parser.sections(), [])
2408+
2409+
@support.requires_resource('cpu')
2410+
def test_clear_many_options(self):
2411+
n = 50000
2412+
parser = configparser.RawConfigParser({"key": "value"})
2413+
parser["section"] = {str(i): str(i) for i in range(n)}
2414+
section = parser["section"]
2415+
self.assertEqual(len(section), n + 1)
2416+
# A quadratic clear() would copy the whole section merged with
2417+
# the defaults for every removed option.
2418+
section.clear()
2419+
self.assertEqual(dict(section), {"key": "value"})
2420+
2421+
23442422
class MiscTestCase(unittest.TestCase):
23452423
def test__all__(self):
23462424
support.check__all__(self, configparser, not_exported={"Error"})
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix quadratic time complexity of :meth:`!clear` and :meth:`!popitem` on
2+
:class:`configparser.ConfigParser` objects and their section proxies.
3+
Clearing a section no longer fails part way with
4+
:exc:`~configparser.InterpolationError` on a value it removes.

0 commit comments

Comments
 (0)