From 087314023f501d28a19d28ea72887de266a652d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 14 Apr 2026 00:32:54 +0200 Subject: [PATCH 1/2] gh-148370: prevent quadratic behavior in `configparser.ParsingError.combine` (GH-148452) (cherry picked from commit 2662db0c45aa16232136628457a53681b6683c25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/configparser.py | 9 ++++++--- Lib/test/test_configparser.py | 13 +++++++++++++ .../2026-04-12-16-40-11.gh-issue-148370.0Li2EK.rst | 2 ++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-04-12-16-40-11.gh-issue-148370.0Li2EK.rst diff --git a/Lib/configparser.py b/Lib/configparser.py index 05b86acb919bfd..71f842b3902e8c 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -316,12 +316,15 @@ def __init__(self, source, *args): def append(self, lineno, line): self.errors.append((lineno, line)) - self.message += '\n\t[line %2d]: %s' % (lineno, repr(line)) + self.message += f'\n\t[line {lineno:2d}]: {line!r}' def combine(self, others): + messages = [self.message] for other in others: - for error in other.errors: - self.append(*error) + for lineno, line in other.errors: + self.errors.append((lineno, line)) + messages.append(f'\n\t[line {lineno:2d}]: {line!r}') + self.message = "".join(messages) return self @staticmethod diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index d4fdd92bdc6b6a..f94832c6d067d4 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -1729,6 +1729,19 @@ def test_error(self): self.assertEqual(e1.message, e2.message) self.assertEqual(repr(e1), repr(e2)) + def test_combine_error_linear_complexity(self): + # Ensure that ParsingError.combine() has linear complexity. + # See https://github.com/python/cpython/issues/148370. + n = 50000 + s = '[*]\n' + (err_line := '=\n') * n + p = configparser.ConfigParser(strict=False) + with self.assertRaises(configparser.ParsingError) as cm: + p.read_string(s) + errlines = cm.exception.message.splitlines() + self.assertEqual(len(errlines), n + 1) + self.assertStartsWith(errlines[0], "Source contains parsing errors: ") + self.assertEqual(errlines[42], f"\t[line {43:2d}]: {err_line!r}") + def test_nosectionerror(self): import pickle e1 = configparser.NoSectionError('section') diff --git a/Misc/NEWS.d/next/Library/2026-04-12-16-40-11.gh-issue-148370.0Li2EK.rst b/Misc/NEWS.d/next/Library/2026-04-12-16-40-11.gh-issue-148370.0Li2EK.rst new file mode 100644 index 00000000000000..3bb662350796f6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-12-16-40-11.gh-issue-148370.0Li2EK.rst @@ -0,0 +1,2 @@ +:mod:`configparser`: prevent quadratic behavior when a :exc:`~configparser.ParsingError` +is raised after a parser fails to parse multiple lines. Patch by Bénédikt Tran. From 151473c0e3e2e9ce24b8d78aae7e4fcda12ea3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 14 Apr 2026 00:42:29 +0200 Subject: [PATCH 2/2] Update Lib/test/test_configparser.py --- Lib/test/test_configparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index f94832c6d067d4..bef3c11b941242 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -1739,7 +1739,7 @@ def test_combine_error_linear_complexity(self): p.read_string(s) errlines = cm.exception.message.splitlines() self.assertEqual(len(errlines), n + 1) - self.assertStartsWith(errlines[0], "Source contains parsing errors: ") + self.assertTrue(errlines[0].startswith("Source contains parsing errors: ")) self.assertEqual(errlines[42], f"\t[line {43:2d}]: {err_line!r}") def test_nosectionerror(self):