From e256f7f385718c9d85f3a729583e694b81b1279d Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Thu, 22 Oct 2026 05:29:07 +0000 Subject: [PATCH] gh-161108: Make configparser clear() linear time 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. --- Lib/configparser.py | 43 +++++++++- Lib/test/test_configparser.py | 78 +++++++++++++++++++ ...-10-22-05-20-18.gh-issue-161108.xOVsRA.rst | 4 + 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-22-05-20-18.gh-issue-161108.xOVsRA.rst diff --git a/Lib/configparser.py b/Lib/configparser.py index f28aba7c3db7b6d..b7a0b0b56893c5f 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -1006,12 +1006,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() @@ -1432,6 +1440,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. diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index f24d7b869991c22..61b8d1c527d3472 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -936,6 +936,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": "", "name4": ""}) + 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': '', + 'name4': ''}) + default = cf[self.default_section] + self.assertEqual(default.popitem(), ('name2', '')) + self.assertEqual(default.popitem(), ('name4', '')) + 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": "", "name4": ""}) + section = cf['section1'] + section.clear() + self.assertEqual(dict(section), {'name2': '', + 'name4': ''}) + 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( f""" @@ -1033,6 +1079,11 @@ def test_interpolation_missing_value(self): 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', ''), @@ -2468,6 +2519,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__( diff --git a/Misc/NEWS.d/next/Library/2026-10-22-05-20-18.gh-issue-161108.xOVsRA.rst b/Misc/NEWS.d/next/Library/2026-10-22-05-20-18.gh-issue-161108.xOVsRA.rst new file mode 100644 index 000000000000000..d58214c823a5525 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-22-05-20-18.gh-issue-161108.xOVsRA.rst @@ -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.