https://github.com/python/cpython/commit/9ac42004b7d56df20bdea8aad1d19e4f90591e02
commit: 9ac42004b7d56df20bdea8aad1d19e4f90591e02
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-27T11:40:12Z
summary:

[3.14] gh-156444: Fix a negated character set with IGNORECASE and LOCALE 
(GH-156445) (GH-156457)

charset_loc_ignore() tested the whole set once per locale case and took
the disjunction, which complements a set before closing it under case
instead of after: [bc] matched b'B', but so did [^bc].  Match both cases
of the character against every set member instead.
(cherry picked from commit 6671d27327dd0518c502aaac9d1b2e079d80ef05)

Co-authored-by: Serhiy Storchaka <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-08-27-08-31-29.gh-issue-156444.klcySy.rst
M Lib/test/test_re.py
M Modules/_sre/sre_lib.h

diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py
index fc052a2f9329ecc..021a8a3fe29cd6f 100644
--- a/Lib/test/test_re.py
+++ b/Lib/test/test_re.py
@@ -1920,6 +1920,28 @@ def test_locale_flag(self):
         self.assertRaises(ValueError, re.compile, b'(?a)', re.LOCALE)
         self.assertRaises(re.PatternError, re.compile, b'(?aL)')
 
+    def test_locale_ignorecase_negated_set(self):
+        IL = re.LOCALE | re.IGNORECASE
+        # [bc] matches b'B', so [^bc] must not.
+        self.assertTrue(re.fullmatch(rb'[bc]', b'B', IL))
+        self.assertIsNone(re.fullmatch(rb'[^bc]', b'B', IL))
+        self.assertIsNone(re.fullmatch(rb'[^b-c]', b'C', IL))
+        self.assertIsNone(re.fullmatch(rb'[^bc]', b'c', IL))
+        self.assertTrue(re.fullmatch(rb'[^bc]', b'a', IL))
+        # A one-member set compiles to NOT_LITERAL_LOC_IGNORE.
+        self.assertIsNone(re.fullmatch(rb'[^b]', b'B', IL))
+        self.assertTrue(re.fullmatch(rb'[^b]', b'a', IL))
+        self.assertIsNone(re.fullmatch(rb'[^\wq]', b'Q', IL))
+        # A sparse set compiles to a bitmap instead of ranges.
+        self.assertTrue(re.fullmatch(rb'[ace]', b'C', IL))
+        self.assertIsNone(re.fullmatch(rb'[^ace]', b'C', IL))
+        self.assertTrue(re.fullmatch(rb'[^ace]', b'b', IL))
+        # An alternation folded into a set puts NEGATE in the middle of it.
+        self.assertIsNone(re.fullmatch(rb'(?:a|[^bc])', b'B', IL))
+        self.assertTrue(re.fullmatch(rb'(?:a|[^bc])', b'A', IL))
+        self.assertIsNone(re.fullmatch(rb'\w(?<!b)', b'B', IL))
+        self.assertTrue(re.fullmatch(rb'\w(?<!b)', b'A', IL))
+
     def test_scoped_flags(self):
         self.assertTrue(re.match(r'(?i:a)b', 'Ab'))
         self.assertIsNone(re.match(r'(?i:a)b', 'aB'))
diff --git 
a/Misc/NEWS.d/next/Library/2026-08-27-08-31-29.gh-issue-156444.klcySy.rst 
b/Misc/NEWS.d/next/Library/2026-08-27-08-31-29.gh-issue-156444.klcySy.rst
new file mode 100644
index 000000000000000..cae73e534c47ddd
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-27-08-31-29.gh-issue-156444.klcySy.rst
@@ -0,0 +1,3 @@
+Fix matching of a negated character set in a bytes pattern compiled with both
+:const:`re.IGNORECASE` and :const:`re.LOCALE`.  The case closure is now applied
+to the members of the set, so that ``[^bc]`` no longer matches ``b'B'``.
diff --git a/Modules/_sre/sre_lib.h b/Modules/_sre/sre_lib.h
index df377905bfae0d0..572901308509ccf 100644
--- a/Modules/_sre/sre_lib.h
+++ b/Modules/_sre/sre_lib.h
@@ -175,16 +175,71 @@ SRE(charset)(SRE_STATE* state, const SRE_CODE* set, 
SRE_CODE ch)
     }
 }
 
+/* Like SRE(charset), but matches both locale cases of ch against every set
+   member.  Testing the whole set once per case would complement it before
+   closing it under case instead of after, so that [^bc] matched b'B'.
+   BIGCHARSET and RANGE_UNI_IGNORE are not handled: they never occur in a
+   set of a bytes pattern. */
 LOCAL(int)
 SRE(charset_loc_ignore)(SRE_STATE* state, const SRE_CODE* set, SRE_CODE ch)
 {
     SRE_CODE lo, up;
-    lo = sre_lower_locale(ch);
-    if (SRE(charset)(state, set, lo))
-       return 1;
+    int ok = 1;
 
+    lo = sre_lower_locale(ch);
     up = sre_upper_locale(ch);
-    return up != lo && SRE(charset)(state, set, up);
+    if (up == lo)
+        return SRE(charset)(state, set, lo);
+
+    for (;;) {
+        switch (*set++) {
+
+        case SRE_OP_FAILURE:
+            return !ok;
+
+        case SRE_OP_LITERAL:
+            /* <LITERAL> <code> */
+            if (lo == set[0] || up == set[0])
+                return ok;
+            set++;
+            break;
+
+        case SRE_OP_CATEGORY:
+            /* <CATEGORY> <code> */
+            if (sre_category(set[0], (int) lo) ||
+                sre_category(set[0], (int) up))
+                return ok;
+            set++;
+            break;
+
+        case SRE_OP_CHARSET:
+            /* <CHARSET> <bitmap> */
+            if ((lo < 256 && (set[lo/SRE_CODE_BITS]
+                              & (1u << (lo & (SRE_CODE_BITS-1))))) ||
+                (up < 256 && (set[up/SRE_CODE_BITS]
+                              & (1u << (up & (SRE_CODE_BITS-1))))))
+                return ok;
+            set += 256/SRE_CODE_BITS;
+            break;
+
+        case SRE_OP_RANGE:
+            /* <RANGE> <lower> <upper> */
+            if ((set[0] <= lo && lo <= set[1]) ||
+                (set[0] <= up && up <= set[1]))
+                return ok;
+            set += 2;
+            break;
+
+        case SRE_OP_NEGATE:
+            ok = !ok;
+            break;
+
+        default:
+            /* internal error -- there's not much we can do about it
+               here, so let's just pretend it didn't match... */
+            return 0;
+        }
+    }
 }
 
 LOCAL(Py_ssize_t) SRE(match)(SRE_STATE* state, const SRE_CODE* pattern, int 
toplevel);

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to