diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py index af6e4612dcfaef5..423b20878543466 100644 --- a/Lib/test/test_re.py +++ b/Lib/test/test_re.py @@ -2712,6 +2712,20 @@ def test_search_anchor_at_beginning(self): # With optimization -- 0.0003 seconds. self.assertLess(stopwatch.seconds, 0.1) + def test_search_anchor_at_beginning_line(self): + # Test "^" anchor with UCS-1, UCS-2 and UCS-4 characters. + p = re.compile('^a', re.MULTILINE) + self.assertEqual([m.span() for m in p.finditer('a\n\xe0a\na')], [(0, 1), (5, 6)]) + self.assertEqual([m.span() for m in p.finditer('a\n\u0430a\na')], [(0, 1), (5, 6)]) + self.assertEqual([m.span() for m in p.finditer('a\n\U0001d49ca\na')], [(0, 1), (5, 6)]) + + def test_search_anchor_at_beginning_line_at_end(self): + # "^" matches the end of the string if the string ends with a newline. + p = re.compile('^', re.MULTILINE) + self.assertEqual([m.span() for m in p.finditer('\xe0\n')], [(0, 0), (2, 2)]) + self.assertEqual([m.span() for m in p.finditer('\u0430\n')], [(0, 0), (2, 2)]) + self.assertEqual([m.span() for m in p.finditer('\U0001d49c\n')], [(0, 0), (2, 2)]) + def test_possessive_quantifiers(self): """Test Possessive Quantifiers Test quantifiers of the form @+ for some repetition operator @, diff --git a/Misc/NEWS.d/next/Library/2026-04-19-23-29-38.gh-issue-148762.HSCJka.rst b/Misc/NEWS.d/next/Library/2026-04-19-23-29-38.gh-issue-148762.HSCJka.rst new file mode 100644 index 000000000000000..e7e3de7a96cbd3b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-19-23-29-38.gh-issue-148762.HSCJka.rst @@ -0,0 +1,2 @@ +Multiline regexes starting with a caret, such as ``re.compile("^foo", +re.MULTILINE)``, now run significantly faster. diff --git a/Modules/_sre/sre_lib.h b/Modules/_sre/sre_lib.h index 6e6ae46f05a50f0..fadfc8c0a1a0618 100644 --- a/Modules/_sre/sre_lib.h +++ b/Modules/_sre/sre_lib.h @@ -1848,6 +1848,29 @@ SRE(search)(SRE_STATE* state, SRE_CODE* pattern) ptr++; RESET_CAPTURE_GROUP(); } + } else if (pattern[0] == SRE_OP_AT && + pattern[1] == SRE_AT_BEGINNING_LINE) { + /* pattern is anchored at the start of a line (MULTILINE "^"). + Only the start of the string and the character after a linebreak + can match, so jump from one line start to the next instead of + trying SRE(match) at every position. */ + end = (SRE_CHAR *)state->end; + TRACE(("|%p|%p|SEARCH AT_BEGINNING_LINE\n", pattern, ptr)); + state->start = state->ptr = ptr; + status = SRE(match)(state, pattern, 1); + state->must_advance = 0; + while (status == 0) { + /* skip to the next linebreak ... */ + while (ptr < end && !SRE_IS_LINEBREAK(*ptr)) + ptr++; + if (ptr >= end) + return 0; + ptr++; /* ... and step past it, onto a line start */ + RESET_CAPTURE_GROUP(); + TRACE(("|%p|%p|SEARCH AT_BEGINNING_LINE\n", pattern, ptr)); + state->start = state->ptr = ptr; + status = SRE(match)(state, pattern, 0); + } } else { /* general case */ assert(ptr <= end);