Skip to content

Commit 2a1282b

Browse files
authored
gh-161108: Make configparser clear() linear time (GH-161109)
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
1 parent 2982d93 commit 2a1282b

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
@@ -1006,12 +1006,20 @@ def popitem(self):
10061006
10071007
The section DEFAULT is never returned because it cannot be removed.
10081008
"""
1009-
for key in self.sections():
1009+
for key in self._sections:
10101010
value = self[key]
10111011
del self[key]
10121012
return key, value
10131013
raise KeyError
10141014

1015+
def clear(self):
1016+
"""Remove all sections from the parser.
1017+
1018+
The section DEFAULT is left intact because it cannot be removed.
1019+
"""
1020+
for section in self.sections():
1021+
del self[section]
1022+
10151023
def optionxform(self, optionstr):
10161024
return optionstr.lower()
10171025

@@ -1432,6 +1440,39 @@ def _options(self):
14321440
else:
14331441
return self._parser.defaults()
14341442

1443+
# MutableMapping.popitem() and MutableMapping.clear() create a new
1444+
# iterator for every removed option, and __iter__() copies the names
1445+
# of all options. Emptying a section that way takes quadratic time.
1446+
1447+
def popitem(self):
1448+
"""Remove an option from the section and return it as
1449+
an (option_name, value) tuple. If no option is present, raise KeyError.
1450+
1451+
Options of the section DEFAULT are only returned for that section
1452+
because they cannot be removed from other sections.
1453+
"""
1454+
if self._name != self._parser.default_section:
1455+
try:
1456+
options = self._parser._sections[self._name]
1457+
except KeyError:
1458+
raise NoSectionError(self._name) from None
1459+
else:
1460+
options = self._parser.defaults()
1461+
for key in options:
1462+
value = self[key]
1463+
del self[key]
1464+
return key, value
1465+
raise KeyError
1466+
1467+
def clear(self):
1468+
"""Remove all options from the section.
1469+
1470+
Options of the section DEFAULT are left intact for other sections
1471+
because they cannot be removed from them.
1472+
"""
1473+
for key in list(self._options()):
1474+
self._parser.remove_option(self._name, key)
1475+
14351476
@property
14361477
def parser(self):
14371478
# 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
@@ -936,6 +936,52 @@ def test_clear(self):
936936
self.assertEqual(set(cf.sections()), set())
937937
self.assertEqual(set(cf[self.default_section].keys()), {'foo'})
938938

939+
def test_section_popitem(self):
940+
cf = self.fromstring(f"""
941+
[section]
942+
name1 {self.delimiters[0]} value1
943+
name2 {self.delimiters[0]} value2
944+
name3 {self.delimiters[0]} value3
945+
""", defaults={"name2": "<default>", "name4": "<default>"})
946+
section = cf['section']
947+
self.assertEqual(section.popitem(), ('name1', 'value1'))
948+
self.assertEqual(section.popitem(), ('name2', 'value2'))
949+
self.assertEqual(section.popitem(), ('name3', 'value3'))
950+
with self.assertRaises(KeyError):
951+
section.popitem()
952+
self.assertEqual(dict(section), {'name2': '<default>',
953+
'name4': '<default>'})
954+
default = cf[self.default_section]
955+
self.assertEqual(default.popitem(), ('name2', '<default>'))
956+
self.assertEqual(default.popitem(), ('name4', '<default>'))
957+
with self.assertRaises(KeyError):
958+
default.popitem()
959+
self.assertEqual(dict(section), {})
960+
del cf['section']
961+
with self.assertRaises(configparser.NoSectionError):
962+
section.popitem()
963+
964+
def test_section_clear(self):
965+
cf = self.fromstring(f"""
966+
[section1]
967+
name1 {self.delimiters[0]} value1
968+
name2 {self.delimiters[0]} value2
969+
[section2]
970+
name3 {self.delimiters[0]} value3
971+
""", defaults={"name2": "<default>", "name4": "<default>"})
972+
section = cf['section1']
973+
section.clear()
974+
self.assertEqual(dict(section), {'name2': '<default>',
975+
'name4': '<default>'})
976+
self.assertEqual(cf.sections(), ['section1', 'section2'])
977+
cf[self.default_section].clear()
978+
self.assertEqual(dict(cf[self.default_section]), {})
979+
self.assertEqual(dict(section), {})
980+
self.assertEqual(dict(cf['section2']), {'name3': 'value3'})
981+
del cf['section1']
982+
with self.assertRaises(configparser.NoSectionError):
983+
section.clear()
984+
939985
def test_setitem(self):
940986
cf = self.fromstring(
941987
f"""
@@ -1033,6 +1079,11 @@ def test_interpolation_missing_value(self):
10331079
e.args, ('name', 'Interpolation Error', '%(reference)s', 'reference')
10341080
)
10351081

1082+
def test_section_clear_does_not_interpolate(self):
1083+
cf = self.get_interpolation_config()
1084+
cf["Interpolation Error"].clear()
1085+
self.assertEqual(cf.options("Interpolation Error"), [])
1086+
10361087
def test_items(self):
10371088
self.check_items_config([
10381089
('default', '<default>'),
@@ -2468,6 +2519,33 @@ def test_option_regex_no_value_does_not_backtrack(self):
24682519
self.assertTrue(parser.has_option("section", "x" + " " * 40000 + "y"))
24692520

24702521

2522+
class MappingComplexityTestCase(unittest.TestCase):
2523+
"""Removing all sections or all options should take linear time."""
2524+
2525+
@support.requires_resource('cpu')
2526+
def test_clear_many_sections(self):
2527+
n = 50000
2528+
parser = configparser.RawConfigParser()
2529+
parser.read_string("".join(f"[{i}]\n" for i in range(n)))
2530+
self.assertEqual(len(parser.sections()), n)
2531+
# A quadratic clear() would copy the names of all remaining
2532+
# sections for every removed section.
2533+
parser.clear()
2534+
self.assertEqual(parser.sections(), [])
2535+
2536+
@support.requires_resource('cpu')
2537+
def test_clear_many_options(self):
2538+
n = 50000
2539+
parser = configparser.RawConfigParser({"key": "value"})
2540+
parser["section"] = {str(i): str(i) for i in range(n)}
2541+
section = parser["section"]
2542+
self.assertEqual(len(section), n + 1)
2543+
# A quadratic clear() would copy the whole section merged with
2544+
# the defaults for every removed option.
2545+
section.clear()
2546+
self.assertEqual(dict(section), {"key": "value"})
2547+
2548+
24712549
class MiscTestCase(unittest.TestCase):
24722550
def test__all__(self):
24732551
support.check__all__(
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)