From 2be8f83e9fa006232137a42bbbcfc2ba64afbfef Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Tue, 4 Aug 2026 11:03:37 +0200 Subject: [PATCH 1/2] [3.10] gh-98820: Fix quadratic time in csv.Sniffer for quoted fields (GH-154867) (cherry picked from commit b30c7fa9edd921a118f286e9f90f560777fa693b) (cherry picked from commit 39dac551f9721da974ccfafaca6b632b3212fcb7) --- Lib/csv.py | 14 +++++++++----- Lib/test/test_csv.py | 7 +++++++ .../2026-07-29-11-20-00.gh-issue-98820.Qm7Hs4.rst | 2 ++ 3 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-29-11-20-00.gh-issue-98820.Qm7Hs4.rst diff --git a/Lib/csv.py b/Lib/csv.py index 5866f3796c6a9cf..745367bddce11cb 100644 --- a/Lib/csv.py +++ b/Lib/csv.py @@ -215,12 +215,16 @@ def _guess_quote_and_delimiter(self, data, delimiters): this way. """ + # The body of a quoted field ends at the first quote which is + # not doubled, as it does for a reader. A lazy ".*?" scans to + # the end of the sample instead, from every start: quadratically. + body = r'(?:(?P=quote){2}|(?!(?P=quote)).)*+' matches = [] - for restr in (r'(?P[^\w\n"\'])(?P ?)(?P["\']).*?(?P=quote)(?P=delim)', # ,".*?", - r'(?:^|\n)(?P["\']).*?(?P=quote)(?P[^\w\n"\'])(?P ?)', # ".*?", - r'(?P[^\w\n"\'])(?P ?)(?P["\']).*?(?P=quote)(?:$|\n)', # ,".*?" - r'(?:^|\n)(?P["\']).*?(?P=quote)(?:$|\n)'): # ".*?" (no delim, no space) - regexp = re.compile(restr, re.DOTALL | re.MULTILINE) + for restr in (r'(?P[^\w\n"\'])(?P ?)(?P["\'])%s(?P=quote)(?P=delim)', # ,"...", + r'(?:^|\n)(?P["\'])%s(?P=quote)(?P[^\w\n"\'])(?P ?)', # "...", + r'(?P[^\w\n"\'])(?P ?)(?P["\'])%s(?P=quote)(?:$|\n)', # ,"..." + r'(?:^|\n)(?P["\'])%s(?P=quote)(?:$|\n)'): # "..." (no delim, no space) + regexp = re.compile(restr % body, re.DOTALL | re.MULTILINE) matches = regexp.findall(data) if matches: break diff --git a/Lib/test/test_csv.py b/Lib/test/test_csv.py index 6ff5bc3d644e69d..f83929b5626939a 100644 --- a/Lib/test/test_csv.py +++ b/Lib/test/test_csv.py @@ -1232,6 +1232,13 @@ def test_sniff_space_delimiter(self): self.assertEqual(dialect.delimiter, ' ') self.assertIs(dialect.doublequote, False) + def test_sniff_quoted_single_column(self): + # gh-98820: this sample used to take minutes. + sniffer = csv.Sniffer() + sample = '"abcdefghijklmnopqrstuvwxyz"\n' * 10000 + with self.assertRaisesRegex(csv.Error, "Could not determine delimiter"): + sniffer.sniff(sample, delimiters=',:|\t') + class NUL: def write(s, *args): diff --git a/Misc/NEWS.d/next/Library/2026-07-29-11-20-00.gh-issue-98820.Qm7Hs4.rst b/Misc/NEWS.d/next/Library/2026-07-29-11-20-00.gh-issue-98820.Qm7Hs4.rst new file mode 100644 index 000000000000000..aa9ae8d937004fe --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-29-11-20-00.gh-issue-98820.Qm7Hs4.rst @@ -0,0 +1,2 @@ +Fix quadratic time in :meth:`csv.Sniffer.sniff` for a sample which contains +quoted fields, in particular for a single column of quoted fields. From 413b9d6371f2f3da1e1127cde8979ab42f3fbb65 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 11 Aug 2026 09:40:05 +0300 Subject: [PATCH 2/2] Match the body of a quoted field without possessive quantifiers They were added in 3.11. An unrolled loop is unambiguous at every position, so it does not backtrack either. Co-Authored-By: Claude Opus 5 (1M context) --- Lib/csv.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/csv.py b/Lib/csv.py index 745367bddce11cb..4e95bf9eb743265 100644 --- a/Lib/csv.py +++ b/Lib/csv.py @@ -218,7 +218,9 @@ def _guess_quote_and_delimiter(self, data, delimiters): # The body of a quoted field ends at the first quote which is # not doubled, as it does for a reader. A lazy ".*?" scans to # the end of the sample instead, from every start: quadratically. - body = r'(?:(?P=quote){2}|(?!(?P=quote)).)*+' + # As an unrolled loop it is unambiguous, so it does not backtrack. + other = r'(?:(?!(?P=quote)).)*' + body = r'%s(?:(?P=quote){2}%s)*' % (other, other) matches = [] for restr in (r'(?P[^\w\n"\'])(?P ?)(?P["\'])%s(?P=quote)(?P=delim)', # ,"...", r'(?:^|\n)(?P["\'])%s(?P=quote)(?P[^\w\n"\'])(?P ?)', # "...",