PyPcre 0.4.0__tar.gz → 0.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {pypcre-0.4.0 → pypcre-0.6.1}/PKG-INFO +110 -17
  2. {pypcre-0.4.0 → pypcre-0.6.1}/PyPcre.egg-info/PKG-INFO +110 -17
  3. {pypcre-0.4.0 → pypcre-0.6.1}/PyPcre.egg-info/SOURCES.txt +21 -0
  4. {pypcre-0.4.0 → pypcre-0.6.1}/README.md +107 -14
  5. {pypcre-0.4.0 → pypcre-0.6.1}/pcre/__init__.py +3 -8
  6. pypcre-0.6.1/pcre/_stdlib_re.py +37 -0
  7. {pypcre-0.4.0 → pypcre-0.6.1}/pcre/cache.py +111 -25
  8. pypcre-0.6.1/pcre/pcre.py +1930 -0
  9. {pypcre-0.4.0 → pypcre-0.6.1}/pcre/re_compat.py +171 -11
  10. pypcre-0.6.1/pcre/threads.py +292 -0
  11. {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/atomic_compat.h +36 -0
  12. {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/cache.c +217 -80
  13. {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/error.c +21 -0
  14. {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/flag.c +8 -4
  15. {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/memory.c +14 -20
  16. pypcre-0.6.1/pcre_ext/pattern_cache.c +431 -0
  17. pypcre-0.6.1/pcre_ext/pcre2.c +6477 -0
  18. {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/pcre2_module.h +20 -1
  19. {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/string_helpers.c +255 -34
  20. {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/util.c +107 -11
  21. {pypcre-0.4.0 → pypcre-0.6.1}/pyproject.toml +4 -4
  22. {pypcre-0.4.0 → pypcre-0.6.1}/setup.py +7 -1
  23. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_api_parity.py +176 -0
  24. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_bytes.py +6 -8
  25. pypcre-0.6.1/tests/test_c_api_audit.py +440 -0
  26. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_cache.py +86 -24
  27. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_cache_global.py +32 -3
  28. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_cache_global_inproc.py +58 -5
  29. pypcre-0.6.1/tests/test_cache_scope_safety.py +276 -0
  30. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_clobber.py +4 -1
  31. pypcre-0.6.1/tests/test_clobber_verify.py +836 -0
  32. pypcre-0.6.1/tests/test_compile_utf_no_check_safety.py +150 -0
  33. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_core.py +2 -0
  34. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_coverage_gaps.py +57 -2
  35. pypcre-0.6.1/tests/test_escape_fastpath.py +83 -0
  36. pypcre-0.6.1/tests/test_expand_fastpath.py +302 -0
  37. pypcre-0.6.1/tests/test_gil_zero_safety.py +245 -0
  38. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_jit.py +7 -2
  39. pypcre-0.6.1/tests/test_jit_anchoring.py +166 -0
  40. pypcre-0.6.1/tests/test_jit_start_optimize_workaround.py +82 -0
  41. pypcre-0.6.1/tests/test_lastindex_ovector_fastpath.py +78 -0
  42. pypcre-0.6.1/tests/test_lastindex_semantics.py +115 -0
  43. pypcre-0.6.1/tests/test_literal_capture_findall_fastpath.py +123 -0
  44. pypcre-0.6.1/tests/test_literal_capture_split_fastpath.py +94 -0
  45. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_memory.py +66 -8
  46. pypcre-0.6.1/tests/test_mmap.py +139 -0
  47. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_module.py +5 -0
  48. pypcre-0.6.1/tests/test_multi_literal_capture_findall_fastpath.py +144 -0
  49. pypcre-0.6.1/tests/test_multi_literal_capture_split_fastpath.py +114 -0
  50. pypcre-0.6.1/tests/test_python_coverage_audit.py +825 -0
  51. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_re_compat.py +2 -3
  52. pypcre-0.6.1/tests/test_stdlib_flag_fastpath.py +111 -0
  53. pypcre-0.6.1/tests/test_sub_count_one_fastpath.py +154 -0
  54. pypcre-0.6.1/tests/test_sub_reference_fastpath.py +140 -0
  55. pypcre-0.6.1/tests/test_template_dispatch_fastpath.py +74 -0
  56. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_threaded_backend.py +26 -4
  57. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_threads.py +126 -10
  58. pypcre-0.4.0/pcre/_stdlib_re.py +0 -28
  59. pypcre-0.4.0/pcre/pcre.py +0 -820
  60. pypcre-0.4.0/pcre/threads.py +0 -181
  61. pypcre-0.4.0/pcre_ext/pattern_cache.c +0 -325
  62. pypcre-0.4.0/pcre_ext/pcre2.c +0 -2662
  63. {pypcre-0.4.0 → pypcre-0.6.1}/LICENSE +0 -0
  64. {pypcre-0.4.0 → pypcre-0.6.1}/MANIFEST.in +0 -0
  65. {pypcre-0.4.0 → pypcre-0.6.1}/PyPcre.egg-info/dependency_links.txt +0 -0
  66. {pypcre-0.4.0 → pypcre-0.6.1}/PyPcre.egg-info/top_level.txt +0 -0
  67. {pypcre-0.4.0 → pypcre-0.6.1}/pcre/error.py +0 -0
  68. {pypcre-0.4.0 → pypcre-0.6.1}/pcre/flags.py +0 -0
  69. {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/jit_support.c +0 -0
  70. {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/pcre2.h +0 -0
  71. {pypcre-0.4.0 → pypcre-0.6.1}/setup.cfg +0 -0
  72. {pypcre-0.4.0 → pypcre-0.6.1}/setup_utils.py +0 -0
  73. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_accuracy.py +0 -0
  74. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_basic.py +0 -0
  75. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_bench_string.py +0 -0
  76. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_benchmark.py +0 -0
  77. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_clobber_thread.py +0 -0
  78. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_errors.py +0 -0
  79. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_flags.py +0 -0
  80. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_pattern.py +0 -0
  81. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_setup_utils_multiarch.py +0 -0
  82. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_simd.py +0 -0
  83. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_transformers_regex.py +0 -0
  84. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_utf8.py +0 -0
  85. {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_version.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPcre
3
- Version: 0.4.0
3
+ Version: 0.6.1
4
4
  Summary: Modern, GIL-friendly, Fast Python bindings for PCRE2 with auto caching and JIT of compiled patterns.
5
5
  Author-email: ModelCloud <qubitium@modelcloud.ai>
6
6
  License-Expression: Apache-2.0
7
- Project-URL: Homepage, https://github.com/ModelCloud/pcre
7
+ Project-URL: Homepage, https://github.com/ModelCloud/PyPcre
8
8
  Keywords: regex,pcre2,bindings
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Programming Language :: C
17
17
  Classifier: Topic :: Software Development :: Libraries
18
18
  Classifier: Topic :: Text Processing :: Linguistic
19
- Requires-Python: >=3.9
19
+ Requires-Python: >=3.10
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Dynamic: license-file
@@ -47,6 +47,9 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
47
47
 
48
48
 
49
49
  ## Latest News 🚀
50
+ * 08/09–08/10/2026 **API performance and safety update**: common regex workloads and ordered `parallel_map(search)` workloads were benchmarked across Python 3.10 and free-threaded Python 3.14t/GIL=0. Compatibility fallbacks preserve complex patterns, subclasses, buffers, and callables. Unsafe UTF bytes compilation is also blocked, with differential, randomized, concurrency, subprocess, and memory-safety coverage. 🧵⚡🛡️
51
+ * 08/08/2026 **0.6.0**: `findall`, `finditer`, `sub`/`subn`, `split`, and `match`/`search`/`fullmatch` are now up to **46x faster** than `stdlib.re` and **48x faster** than `regex` on `finditer`/`findall` workloads, **13x** on `split`, and **2–9x** on `sub`/`subn` backref workloads, with full `re` semantics. Free-threaded `findall` reaches **13.8x** vs `re` on 8 threads. 🚀⚡
52
+ * 07/27/2026 [0.5.0](https://github.com/ModelCloud/PyPcre/releases/tag/v0.5.0): Zero-copy buffer-protocol subject support (`mmap.mmap`, `bytearray`, `array.array`) with UTF-8 validation and GIL=0-safe memory pinning. 🗂️⚡
50
53
  * 07/24/2026 [0.4.0](https://github.com/ModelCloud/PyPcre/releases/tag/v0.4.0): C extension hardening (memory/pointer safety, bounds checks, atomic allocator init), GIL=0 safety verified, vectorized UTF-8 index/offset conversion, GIL-release threshold for small calls, C `findall` implementation, and README competitor benchmarks. 🛡️⚡
51
54
  * 04/13/2026 [0.3.0](https://github.com/ModelCloud/PyPcre/releases/tag/v0.3.0): Lower-overhead public `Match` objects, faster hot-path `match()` / `search()` / `fullmatch()` / `findall()`, and tighter free-threaded execution. ⚡
52
55
  * 03/22/2026 [0.2.15](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.15): Python 3.15 `re` compatibility (`prefixmatch`, `NOFLAG`) ✅
@@ -66,10 +69,10 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
66
69
 
67
70
  - 🧬 **Full power of PCRE2**: PyPcre uses the real `PCRE2` engine, so you get native compile options, semantics, JIT, and upstream tuning.
68
71
  - 🔥 **More expressive regex syntax**: `PCRE2` supports constructs beyond stdlib `re`, including atomic groups `(?>...)`, possessive quantifiers `++`, branch-reset groups `(?|...)`, richer lookarounds, and backtracking control verbs like `(*SKIP)(*FAIL)`.
69
- - 🧵 **Thread-safe into `nogil`**: PyPcre is built for `PYTHON_GIL=0`, with CI coverage, lock-aware caches, reusable match/JIT resources, and `parallel_map()` for multi-subject fan-out.
72
+ - 🧵 **Thread-safe into `nogil`**: PyPcre is built for `PYTHON_GIL=0`, with CI coverage and `parallel_map()` for multi-subject fan-out.
70
73
  - ⚡ **Fast on real workloads**: `PCRE2` JIT plus cached compiled patterns lets PyPcre match or beat `re` and `regex` on many common scans, especially multiline searches, lookaround-heavy patterns, and free-threaded execution.
71
74
  - 🛡️ **Safer operational story**: PyPcre prefers the system `libpcre2-8` shared library so normal OS package updates can bring security and bug-fix benefits without a bundled fork.
72
- - ✅ **Validated thoroughly**: the project runs API tests, fuzz tests, memory-safety checks, local `valgrind` leak checks, and `massif` heap profiles. Recent local profiling found `0` definite leaks and `0` possible leaks in both the public API and raw binding paths.
75
+ - ✅ **Validated thoroughly**: the project runs API, fuzz, concurrency, and memory-safety tests.
73
76
 
74
77
  ### Quick Comparison 🥊
75
78
 
@@ -86,17 +89,69 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
86
89
 
87
90
  ### Benchmark Highlights 🏁
88
91
 
89
- Measured on a `Python 3.14.5` free-threaded build on x86_64 Linux with compiled-pattern reuse and JIT enabled. Times are the best of several runs; lower is better. Only workloads where PyPcre is decisively faster than both `stdlib.re` and `regex` are shown.
90
-
91
- A reproducible version of this benchmark lives in [`benchmarks/competitor_bench.py`](benchmarks/competitor_bench.py).
92
+ The tables below summarize representative public workloads. Lower is better.
93
+
94
+ #### Fan-out and API speedups
95
+
96
+ Both interpreter runs used the same scheduler policy. The host reports 12
97
+ performance logical CPUs and 4 efficiency logical CPUs; macOS does not provide
98
+ an unprivileged hard per-process CPU mask, so these measurements record the
99
+ topology rather than claiming hard CPU affinity.
100
+
101
+ | Workload | Python 3.10 | Python 3.14t/GIL=0 |
102
+ | --- | ---: | ---: |
103
+ | `parallel_map(search)`, 16 × 1 MiB subjects, 12 workers | **8.57x** | **7.85x** |
104
+ | `parallel_map(findall)`, 48 × 1 MiB subjects, 12 workers | **11.51x** | **11.25x** |
105
+ | No-op `escape("literal")` | **5.0x** | **3.6x** |
106
+ | No-op `escape(b"literal")` | **6.9x** | **6.5x** |
107
+ | Bound literal `sub(..., count=1)` | **4.7x** | **4.3x** |
108
+ | Bound numeric-reference `sub(..., count=1)` | **4.3x** | **4.1x** |
109
+ | Bound explicit-reference `sub(..., count=1)` | **4.6x** | **4.3x** |
110
+ | Bound named-reference `sub(..., count=1)` | **4.2x** | **4.3x** |
111
+ | Bound numeric-reference `sub(..., count=2)` | **4.6x** | **4.2x** |
112
+ | Bound numeric-reference `sub(..., count=4)` | **6.0x** | **5.1x** |
113
+ | Bound numeric-reference `sub(..., count=8)` | **8.6x** | **6.1x** |
114
+ | Cached compile with `re.I` | **6.7x** | **6.9x** |
115
+ | Cached compile with `re.I|re.M|re.S|re.X` | **6.2x** | **6.4x** |
116
+ | First-read `lastindex` cost, sole capture | **11.8x** | **8.2x** |
117
+ | Deprecated `template()` compatibility call | **6.2x** | **1.1x** |
118
+ | Literal-capture `findall`, 100 matches | **5.0x** | **6.2x** |
119
+ | Literal-capture `findall`, 500 matches | **7.0x** | **8.4x** |
120
+ | Two literal captures `findall`, 100 matches | **7.5x** | **10.8x** |
121
+ | Two literal captures `findall`, 500 matches | **13.5x** | **18.3x** |
122
+ | Eight literal captures `findall`, 500 matches | **24.5x** | **29.4x** |
123
+ | Three literal captures `split`, 500 captures | **4.5x** | **6.5x** |
124
+ | Eight literal captures `split`, 500 captures | **6.6x** | **9.0x** |
125
+ | Literal-capture `split`, 100 captures | **2.6x** | **3.5x** |
126
+ | Literal-capture `split`, 2,000 captures | **3.1x** | **3.8x** |
127
+ | Bound one-character literal `Pattern.split` | **2.1x** | **1.7x** |
128
+ | Bound backreference `sub` hot path | **1.38 μs** | **1.14 μs** |
129
+ | One-match numeric-reference `Pattern.sub` | **0.45 μs** | **0.34 μs** |
130
+ | One-match explicit-reference `Pattern.sub` | **0.45 μs** | **0.31 μs** |
131
+ | One-match named-reference `Pattern.sub` | **0.46 μs** | **0.33 μs** |
132
+ | Repeated call-local `Match.groups()` | **~0.05 μs** | **~0.05 μs** |
133
+ | Call-local `Match.expand(r"[\\1]")` | **0.07 μs** | **0.07 μs** |
134
+ | Call-local `Match.expand(r"[\\g<1>]")` | **0.11 μs** | **0.08 μs** |
135
+ | Call-local `Match.expand(r"[\\g<word>]")` | **0.13 μs** | **0.11 μs** |
136
+ | Call-local two-name `Match.expand` | **0.18 μs** | **0.15 μs** |
137
+ | Call-local three-name `Match.expand` | **0.23 μs** | **0.20 μs** |
138
+ | Call-local eight-name `Match.expand` | **0.44 μs** | **0.39 μs** |
139
+ | Literal-backslash + named `Match.expand` | **0.12 μs** | **0.10 μs** |
140
+ | Named + backslash-suffix `Match.expand` | **0.16 μs** | **0.13 μs** |
141
+ | Repeated default `compile("(x)")` | **0.49 μs** | **0.38 μs** |
142
+ | Repeated integer-flagged `compile("x", CASELESS)` | **1.16 μs** | **0.81 μs** |
143
+
144
+ The parallel figures are serial-to-parallel speedups and preserve input order
145
+ and exception behavior. They were measured with the same scheduler policy for
146
+ both interpreters on the Apple host.
92
147
 
93
148
  #### `findall` — large multiline and lookaround workloads
94
149
 
95
150
  | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
96
151
  | --- | ---: | ---: | ---: | --- |
97
- | Extract `WARN` / `ERROR` lines (multiline) | `0.60` | `29.35` | `30.85` | **49x** vs `re`, **52x** vs `regex` |
98
- | Per-line full-name extraction (multiline) | `1.02` | `29.50` | `15.95` | **29x** vs `re`, **16x** vs `regex` |
99
- | Lookbehind + negative-lookahead tokens | `3.78` | `11.81` | `10.42` | **3.1x** vs `re`, **2.8x** vs `regex` |
152
+ | Extract `WARN` / `ERROR` lines (multiline) | `0.471` | `15.098` | `16.281` | **32.1x** vs `re`, **34.6x** vs `regex` |
153
+ | Per-line full-name extraction (multiline) | `0.588` | `14.305` | `8.000` | **24.3x** vs `re`, **13.6x** vs `regex` |
154
+ | Lookbehind + negative-lookahead tokens | `1.013` | `6.129` | `5.267` | **6.1x** vs `re`, **5.2x** vs `regex` |
100
155
 
101
156
  Patterns used:
102
157
 
@@ -111,19 +166,51 @@ Patterns used:
111
166
 
112
167
  #### `finditer` — same workloads
113
168
 
169
+ Measured on Python 3.10.11 arm64 with compiled-pattern reuse and JIT enabled. A reproducible version lives in [`benchmarks/finditer_bench.py`](benchmarks/finditer_bench.py).
170
+
171
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
172
+ | --- | ---: | ---: | ---: | --- |
173
+ | Extract `WARN` / `ERROR` lines | `0.449` | `17.145` | `16.175` | **38.2x** vs `re`, **36.0x** vs `regex` |
174
+ | Per-line full-name extraction | `0.564` | `16.258` | `7.874` | **28.8x** vs `re`, **14.0x** vs `regex` |
175
+ | Lookbehind + negative lookahead | `1.942` | `8.548` | `6.037` | **4.4x** vs `re`, **3.1x** vs `regex` |
176
+
177
+ #### `sub` / `subn` — high-volume replacement workloads
178
+
179
+ Measured on Python 3.10.11 arm64 with compiled-pattern reuse and JIT enabled. Values are medians of three outer runs; lower is better. The benchmark replaces 100,000 space-separated tokens.
180
+
181
+ A reproducible version lives in [`benchmarks/sub_bench.py`](benchmarks/sub_bench.py).
182
+
114
183
  | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
115
184
  | --- | ---: | ---: | ---: | --- |
116
- | Extract `WARN` / `ERROR` lines | `0.60` | `29.33` | `31.23` | **49x** vs `re`, **52x** vs `regex` |
117
- | Per-line full-name extraction | `1.02` | `30.09` | `16.25` | **29x** vs `re`, **16x** vs `regex` |
185
+ | Literal replacement (`\w+` → `[X]`) | `3.127` | `6.489` | `9.992` | **2.1x** vs `re`, **3.2x** vs `regex` |
186
+ | Single numeric backref (`(w)\d+` → `[\1]`) | `4.003` | `30.098` | `12.481` | **7.5x** vs `re`, **3.1x** vs `regex` |
187
+ | Two numeric backrefs (`(w)(\d+)` → `\2-\1`) | `9.940` | `35.052` | `16.201` | **3.5x** vs `re`, **1.6x** vs `regex` |
188
+ | Named backref (`(?P<g>\w+)` → `<\g<g>>`) | `9.765` | `32.434` | `15.216` | **3.3x** vs `re`, **1.6x** vs `regex` |
189
+
190
+ #### `split` — high-volume delimiter workloads
191
+
192
+ Measured on Python 3.10.11 arm64 with compiled-pattern reuse and JIT enabled. Values are medians of three outer runs; lower is better. The benchmark splits 100,000 space-separated tokens.
193
+
194
+ A reproducible version lives in [`benchmarks/split_bench.py`](benchmarks/split_bench.py).
195
+
196
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
197
+ | --- | ---: | ---: | ---: | --- |
198
+ | Delimiter no group (`\s+`) | `3.349` | `8.013` | `9.913` | **2.4x** vs `re`, **3.0x** vs `regex` |
199
+ | Delimiter with group (`(\s+)`) | `5.080` | `9.142` | `12.374` | **1.8x** vs `re`, **2.4x** vs `regex` |
200
+ | Single char (` `) | `1.484` | `1.666` | `6.963` | **1.1x** vs `re`, **4.7x** vs `regex` |
201
+ | Single char with group (`( )`) | `1.772` | `4.203` | `8.875` | **2.4x** vs `re`, **5.0x** vs `regex` |
202
+ | Empty pattern (`''`) | `35.736` | `18.386` | `35.801` | **0.5x** vs `re`, parity vs `regex` |
118
203
 
119
204
  ### Free-Threaded Benchmark Highlights 🧵
120
205
 
121
- Measured in the same environment with `8` threads fanning out over independent copies of each workload. Times are the best of several runs; lower is better.
206
+ Measured on the same Apple arm64 `Python 3.14.0rc2` free-threaded build with `8` threads fanning out over split copies of each workload. Values are medians of three outer runs; lower is better.
207
+
208
+ A reproducible version lives in [`benchmarks/free_threaded_bench.py`](benchmarks/free_threaded_bench.py).
122
209
 
123
210
  | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
124
211
  | --- | ---: | ---: | ---: | --- |
125
- | Extract `WARN` / `ERROR` lines (`findall`) | `3.26` | `32.57` | `34.41` | **10.0x** vs `re`, **10.6x** vs `regex` |
126
- | Per-line full-name extraction (`findall`) | `3.18` | `31.84` | `17.31` | **10.0x** vs `re`, **5.4x** vs `regex` |
212
+ | Extract `WARN` / `ERROR` lines (`findall`) | `0.401` | `2.430` | `2.626` | **6.1x** vs `re`, **6.6x** vs `regex` |
213
+ | Per-line full-name extraction (`findall`) | `0.462` | `2.351` | `1.477` | **5.1x** vs `re`, **3.2x** vs `regex` |
127
214
 
128
215
  PyPcre is the stronger all-around choice when you want more than the baseline: full `PCRE2` features, more expressive syntax, JIT, explicit free-threaded support, and a stable `re`-compatible API surface. It keeps Python ergonomics while giving you a substantially more capable engine. 🚀
129
216
 
@@ -269,7 +356,13 @@ the conversion without repeating the flag.
269
356
  - `pcre.configure(jit=False)` disables JIT globally. `Flag.JIT` and
270
357
  `Flag.NO_JIT` let you override that per pattern.
271
358
  - `pcre.set_cache_limit()`, `pcre.get_cache_limit()`, and `pcre.clear_cache()`
272
- control the high-level compile cache.
359
+ control every high-level compile/template helper cache in the active context.
360
+ A zero limit disables them, and `None` uses a 256-entry hard safety ceiling
361
+ rather than permitting unbounded growth. High-level cache entries never cross
362
+ thread scope in the default thread-local strategy; a clear invalidates live
363
+ workers' high-level helper caches on their next cache-backed call. Backend
364
+ scratch buffers remain thread-scoped and are released when that thread exits.
365
+ Oversized patterns and templates are not retained.
273
366
  - `pcre.configure_threads()`, `pcre.configure_thread_pool()`,
274
367
  `shutdown_thread_pool()`, `Flag.THREADS`, and `Flag.NO_THREADS` are available
275
368
  if you want to opt into or restrict threaded execution.
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPcre
3
- Version: 0.4.0
3
+ Version: 0.6.1
4
4
  Summary: Modern, GIL-friendly, Fast Python bindings for PCRE2 with auto caching and JIT of compiled patterns.
5
5
  Author-email: ModelCloud <qubitium@modelcloud.ai>
6
6
  License-Expression: Apache-2.0
7
- Project-URL: Homepage, https://github.com/ModelCloud/pcre
7
+ Project-URL: Homepage, https://github.com/ModelCloud/PyPcre
8
8
  Keywords: regex,pcre2,bindings
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Programming Language :: C
17
17
  Classifier: Topic :: Software Development :: Libraries
18
18
  Classifier: Topic :: Text Processing :: Linguistic
19
- Requires-Python: >=3.9
19
+ Requires-Python: >=3.10
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Dynamic: license-file
@@ -47,6 +47,9 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
47
47
 
48
48
 
49
49
  ## Latest News 🚀
50
+ * 08/09–08/10/2026 **API performance and safety update**: common regex workloads and ordered `parallel_map(search)` workloads were benchmarked across Python 3.10 and free-threaded Python 3.14t/GIL=0. Compatibility fallbacks preserve complex patterns, subclasses, buffers, and callables. Unsafe UTF bytes compilation is also blocked, with differential, randomized, concurrency, subprocess, and memory-safety coverage. 🧵⚡🛡️
51
+ * 08/08/2026 **0.6.0**: `findall`, `finditer`, `sub`/`subn`, `split`, and `match`/`search`/`fullmatch` are now up to **46x faster** than `stdlib.re` and **48x faster** than `regex` on `finditer`/`findall` workloads, **13x** on `split`, and **2–9x** on `sub`/`subn` backref workloads, with full `re` semantics. Free-threaded `findall` reaches **13.8x** vs `re` on 8 threads. 🚀⚡
52
+ * 07/27/2026 [0.5.0](https://github.com/ModelCloud/PyPcre/releases/tag/v0.5.0): Zero-copy buffer-protocol subject support (`mmap.mmap`, `bytearray`, `array.array`) with UTF-8 validation and GIL=0-safe memory pinning. 🗂️⚡
50
53
  * 07/24/2026 [0.4.0](https://github.com/ModelCloud/PyPcre/releases/tag/v0.4.0): C extension hardening (memory/pointer safety, bounds checks, atomic allocator init), GIL=0 safety verified, vectorized UTF-8 index/offset conversion, GIL-release threshold for small calls, C `findall` implementation, and README competitor benchmarks. 🛡️⚡
51
54
  * 04/13/2026 [0.3.0](https://github.com/ModelCloud/PyPcre/releases/tag/v0.3.0): Lower-overhead public `Match` objects, faster hot-path `match()` / `search()` / `fullmatch()` / `findall()`, and tighter free-threaded execution. ⚡
52
55
  * 03/22/2026 [0.2.15](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.15): Python 3.15 `re` compatibility (`prefixmatch`, `NOFLAG`) ✅
@@ -66,10 +69,10 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
66
69
 
67
70
  - 🧬 **Full power of PCRE2**: PyPcre uses the real `PCRE2` engine, so you get native compile options, semantics, JIT, and upstream tuning.
68
71
  - 🔥 **More expressive regex syntax**: `PCRE2` supports constructs beyond stdlib `re`, including atomic groups `(?>...)`, possessive quantifiers `++`, branch-reset groups `(?|...)`, richer lookarounds, and backtracking control verbs like `(*SKIP)(*FAIL)`.
69
- - 🧵 **Thread-safe into `nogil`**: PyPcre is built for `PYTHON_GIL=0`, with CI coverage, lock-aware caches, reusable match/JIT resources, and `parallel_map()` for multi-subject fan-out.
72
+ - 🧵 **Thread-safe into `nogil`**: PyPcre is built for `PYTHON_GIL=0`, with CI coverage and `parallel_map()` for multi-subject fan-out.
70
73
  - ⚡ **Fast on real workloads**: `PCRE2` JIT plus cached compiled patterns lets PyPcre match or beat `re` and `regex` on many common scans, especially multiline searches, lookaround-heavy patterns, and free-threaded execution.
71
74
  - 🛡️ **Safer operational story**: PyPcre prefers the system `libpcre2-8` shared library so normal OS package updates can bring security and bug-fix benefits without a bundled fork.
72
- - ✅ **Validated thoroughly**: the project runs API tests, fuzz tests, memory-safety checks, local `valgrind` leak checks, and `massif` heap profiles. Recent local profiling found `0` definite leaks and `0` possible leaks in both the public API and raw binding paths.
75
+ - ✅ **Validated thoroughly**: the project runs API, fuzz, concurrency, and memory-safety tests.
73
76
 
74
77
  ### Quick Comparison 🥊
75
78
 
@@ -86,17 +89,69 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
86
89
 
87
90
  ### Benchmark Highlights 🏁
88
91
 
89
- Measured on a `Python 3.14.5` free-threaded build on x86_64 Linux with compiled-pattern reuse and JIT enabled. Times are the best of several runs; lower is better. Only workloads where PyPcre is decisively faster than both `stdlib.re` and `regex` are shown.
90
-
91
- A reproducible version of this benchmark lives in [`benchmarks/competitor_bench.py`](benchmarks/competitor_bench.py).
92
+ The tables below summarize representative public workloads. Lower is better.
93
+
94
+ #### Fan-out and API speedups
95
+
96
+ Both interpreter runs used the same scheduler policy. The host reports 12
97
+ performance logical CPUs and 4 efficiency logical CPUs; macOS does not provide
98
+ an unprivileged hard per-process CPU mask, so these measurements record the
99
+ topology rather than claiming hard CPU affinity.
100
+
101
+ | Workload | Python 3.10 | Python 3.14t/GIL=0 |
102
+ | --- | ---: | ---: |
103
+ | `parallel_map(search)`, 16 × 1 MiB subjects, 12 workers | **8.57x** | **7.85x** |
104
+ | `parallel_map(findall)`, 48 × 1 MiB subjects, 12 workers | **11.51x** | **11.25x** |
105
+ | No-op `escape("literal")` | **5.0x** | **3.6x** |
106
+ | No-op `escape(b"literal")` | **6.9x** | **6.5x** |
107
+ | Bound literal `sub(..., count=1)` | **4.7x** | **4.3x** |
108
+ | Bound numeric-reference `sub(..., count=1)` | **4.3x** | **4.1x** |
109
+ | Bound explicit-reference `sub(..., count=1)` | **4.6x** | **4.3x** |
110
+ | Bound named-reference `sub(..., count=1)` | **4.2x** | **4.3x** |
111
+ | Bound numeric-reference `sub(..., count=2)` | **4.6x** | **4.2x** |
112
+ | Bound numeric-reference `sub(..., count=4)` | **6.0x** | **5.1x** |
113
+ | Bound numeric-reference `sub(..., count=8)` | **8.6x** | **6.1x** |
114
+ | Cached compile with `re.I` | **6.7x** | **6.9x** |
115
+ | Cached compile with `re.I|re.M|re.S|re.X` | **6.2x** | **6.4x** |
116
+ | First-read `lastindex` cost, sole capture | **11.8x** | **8.2x** |
117
+ | Deprecated `template()` compatibility call | **6.2x** | **1.1x** |
118
+ | Literal-capture `findall`, 100 matches | **5.0x** | **6.2x** |
119
+ | Literal-capture `findall`, 500 matches | **7.0x** | **8.4x** |
120
+ | Two literal captures `findall`, 100 matches | **7.5x** | **10.8x** |
121
+ | Two literal captures `findall`, 500 matches | **13.5x** | **18.3x** |
122
+ | Eight literal captures `findall`, 500 matches | **24.5x** | **29.4x** |
123
+ | Three literal captures `split`, 500 captures | **4.5x** | **6.5x** |
124
+ | Eight literal captures `split`, 500 captures | **6.6x** | **9.0x** |
125
+ | Literal-capture `split`, 100 captures | **2.6x** | **3.5x** |
126
+ | Literal-capture `split`, 2,000 captures | **3.1x** | **3.8x** |
127
+ | Bound one-character literal `Pattern.split` | **2.1x** | **1.7x** |
128
+ | Bound backreference `sub` hot path | **1.38 μs** | **1.14 μs** |
129
+ | One-match numeric-reference `Pattern.sub` | **0.45 μs** | **0.34 μs** |
130
+ | One-match explicit-reference `Pattern.sub` | **0.45 μs** | **0.31 μs** |
131
+ | One-match named-reference `Pattern.sub` | **0.46 μs** | **0.33 μs** |
132
+ | Repeated call-local `Match.groups()` | **~0.05 μs** | **~0.05 μs** |
133
+ | Call-local `Match.expand(r"[\\1]")` | **0.07 μs** | **0.07 μs** |
134
+ | Call-local `Match.expand(r"[\\g<1>]")` | **0.11 μs** | **0.08 μs** |
135
+ | Call-local `Match.expand(r"[\\g<word>]")` | **0.13 μs** | **0.11 μs** |
136
+ | Call-local two-name `Match.expand` | **0.18 μs** | **0.15 μs** |
137
+ | Call-local three-name `Match.expand` | **0.23 μs** | **0.20 μs** |
138
+ | Call-local eight-name `Match.expand` | **0.44 μs** | **0.39 μs** |
139
+ | Literal-backslash + named `Match.expand` | **0.12 μs** | **0.10 μs** |
140
+ | Named + backslash-suffix `Match.expand` | **0.16 μs** | **0.13 μs** |
141
+ | Repeated default `compile("(x)")` | **0.49 μs** | **0.38 μs** |
142
+ | Repeated integer-flagged `compile("x", CASELESS)` | **1.16 μs** | **0.81 μs** |
143
+
144
+ The parallel figures are serial-to-parallel speedups and preserve input order
145
+ and exception behavior. They were measured with the same scheduler policy for
146
+ both interpreters on the Apple host.
92
147
 
93
148
  #### `findall` — large multiline and lookaround workloads
94
149
 
95
150
  | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
96
151
  | --- | ---: | ---: | ---: | --- |
97
- | Extract `WARN` / `ERROR` lines (multiline) | `0.60` | `29.35` | `30.85` | **49x** vs `re`, **52x** vs `regex` |
98
- | Per-line full-name extraction (multiline) | `1.02` | `29.50` | `15.95` | **29x** vs `re`, **16x** vs `regex` |
99
- | Lookbehind + negative-lookahead tokens | `3.78` | `11.81` | `10.42` | **3.1x** vs `re`, **2.8x** vs `regex` |
152
+ | Extract `WARN` / `ERROR` lines (multiline) | `0.471` | `15.098` | `16.281` | **32.1x** vs `re`, **34.6x** vs `regex` |
153
+ | Per-line full-name extraction (multiline) | `0.588` | `14.305` | `8.000` | **24.3x** vs `re`, **13.6x** vs `regex` |
154
+ | Lookbehind + negative-lookahead tokens | `1.013` | `6.129` | `5.267` | **6.1x** vs `re`, **5.2x** vs `regex` |
100
155
 
101
156
  Patterns used:
102
157
 
@@ -111,19 +166,51 @@ Patterns used:
111
166
 
112
167
  #### `finditer` — same workloads
113
168
 
169
+ Measured on Python 3.10.11 arm64 with compiled-pattern reuse and JIT enabled. A reproducible version lives in [`benchmarks/finditer_bench.py`](benchmarks/finditer_bench.py).
170
+
171
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
172
+ | --- | ---: | ---: | ---: | --- |
173
+ | Extract `WARN` / `ERROR` lines | `0.449` | `17.145` | `16.175` | **38.2x** vs `re`, **36.0x** vs `regex` |
174
+ | Per-line full-name extraction | `0.564` | `16.258` | `7.874` | **28.8x** vs `re`, **14.0x** vs `regex` |
175
+ | Lookbehind + negative lookahead | `1.942` | `8.548` | `6.037` | **4.4x** vs `re`, **3.1x** vs `regex` |
176
+
177
+ #### `sub` / `subn` — high-volume replacement workloads
178
+
179
+ Measured on Python 3.10.11 arm64 with compiled-pattern reuse and JIT enabled. Values are medians of three outer runs; lower is better. The benchmark replaces 100,000 space-separated tokens.
180
+
181
+ A reproducible version lives in [`benchmarks/sub_bench.py`](benchmarks/sub_bench.py).
182
+
114
183
  | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
115
184
  | --- | ---: | ---: | ---: | --- |
116
- | Extract `WARN` / `ERROR` lines | `0.60` | `29.33` | `31.23` | **49x** vs `re`, **52x** vs `regex` |
117
- | Per-line full-name extraction | `1.02` | `30.09` | `16.25` | **29x** vs `re`, **16x** vs `regex` |
185
+ | Literal replacement (`\w+` → `[X]`) | `3.127` | `6.489` | `9.992` | **2.1x** vs `re`, **3.2x** vs `regex` |
186
+ | Single numeric backref (`(w)\d+` → `[\1]`) | `4.003` | `30.098` | `12.481` | **7.5x** vs `re`, **3.1x** vs `regex` |
187
+ | Two numeric backrefs (`(w)(\d+)` → `\2-\1`) | `9.940` | `35.052` | `16.201` | **3.5x** vs `re`, **1.6x** vs `regex` |
188
+ | Named backref (`(?P<g>\w+)` → `<\g<g>>`) | `9.765` | `32.434` | `15.216` | **3.3x** vs `re`, **1.6x** vs `regex` |
189
+
190
+ #### `split` — high-volume delimiter workloads
191
+
192
+ Measured on Python 3.10.11 arm64 with compiled-pattern reuse and JIT enabled. Values are medians of three outer runs; lower is better. The benchmark splits 100,000 space-separated tokens.
193
+
194
+ A reproducible version lives in [`benchmarks/split_bench.py`](benchmarks/split_bench.py).
195
+
196
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
197
+ | --- | ---: | ---: | ---: | --- |
198
+ | Delimiter no group (`\s+`) | `3.349` | `8.013` | `9.913` | **2.4x** vs `re`, **3.0x** vs `regex` |
199
+ | Delimiter with group (`(\s+)`) | `5.080` | `9.142` | `12.374` | **1.8x** vs `re`, **2.4x** vs `regex` |
200
+ | Single char (` `) | `1.484` | `1.666` | `6.963` | **1.1x** vs `re`, **4.7x** vs `regex` |
201
+ | Single char with group (`( )`) | `1.772` | `4.203` | `8.875` | **2.4x** vs `re`, **5.0x** vs `regex` |
202
+ | Empty pattern (`''`) | `35.736` | `18.386` | `35.801` | **0.5x** vs `re`, parity vs `regex` |
118
203
 
119
204
  ### Free-Threaded Benchmark Highlights 🧵
120
205
 
121
- Measured in the same environment with `8` threads fanning out over independent copies of each workload. Times are the best of several runs; lower is better.
206
+ Measured on the same Apple arm64 `Python 3.14.0rc2` free-threaded build with `8` threads fanning out over split copies of each workload. Values are medians of three outer runs; lower is better.
207
+
208
+ A reproducible version lives in [`benchmarks/free_threaded_bench.py`](benchmarks/free_threaded_bench.py).
122
209
 
123
210
  | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
124
211
  | --- | ---: | ---: | ---: | --- |
125
- | Extract `WARN` / `ERROR` lines (`findall`) | `3.26` | `32.57` | `34.41` | **10.0x** vs `re`, **10.6x** vs `regex` |
126
- | Per-line full-name extraction (`findall`) | `3.18` | `31.84` | `17.31` | **10.0x** vs `re`, **5.4x** vs `regex` |
212
+ | Extract `WARN` / `ERROR` lines (`findall`) | `0.401` | `2.430` | `2.626` | **6.1x** vs `re`, **6.6x** vs `regex` |
213
+ | Per-line full-name extraction (`findall`) | `0.462` | `2.351` | `1.477` | **5.1x** vs `re`, **3.2x** vs `regex` |
127
214
 
128
215
  PyPcre is the stronger all-around choice when you want more than the baseline: full `PCRE2` features, more expressive syntax, JIT, explicit free-threaded support, and a stable `re`-compatible API surface. It keeps Python ergonomics while giving you a substantially more capable engine. 🚀
129
216
 
@@ -269,7 +356,13 @@ the conversion without repeating the flag.
269
356
  - `pcre.configure(jit=False)` disables JIT globally. `Flag.JIT` and
270
357
  `Flag.NO_JIT` let you override that per pattern.
271
358
  - `pcre.set_cache_limit()`, `pcre.get_cache_limit()`, and `pcre.clear_cache()`
272
- control the high-level compile cache.
359
+ control every high-level compile/template helper cache in the active context.
360
+ A zero limit disables them, and `None` uses a 256-entry hard safety ceiling
361
+ rather than permitting unbounded growth. High-level cache entries never cross
362
+ thread scope in the default thread-local strategy; a clear invalidates live
363
+ workers' high-level helper caches on their next cache-backed call. Backend
364
+ scratch buffers remain thread-scoped and are released when that thread exits.
365
+ Oversized patterns and templates are not retained.
273
366
  - `pcre.configure_threads()`, `pcre.configure_thread_pool()`,
274
367
  `shutdown_thread_pool()`, `Flag.THREADS`, and `Flag.NO_THREADS` are available
275
368
  if you want to opt into or restrict threaded execution.
@@ -34,22 +34,43 @@ tests/test_basic.py
34
34
  tests/test_bench_string.py
35
35
  tests/test_benchmark.py
36
36
  tests/test_bytes.py
37
+ tests/test_c_api_audit.py
37
38
  tests/test_cache.py
38
39
  tests/test_cache_global.py
39
40
  tests/test_cache_global_inproc.py
41
+ tests/test_cache_scope_safety.py
40
42
  tests/test_clobber.py
41
43
  tests/test_clobber_thread.py
44
+ tests/test_clobber_verify.py
45
+ tests/test_compile_utf_no_check_safety.py
42
46
  tests/test_core.py
43
47
  tests/test_coverage_gaps.py
44
48
  tests/test_errors.py
49
+ tests/test_escape_fastpath.py
50
+ tests/test_expand_fastpath.py
45
51
  tests/test_flags.py
52
+ tests/test_gil_zero_safety.py
46
53
  tests/test_jit.py
54
+ tests/test_jit_anchoring.py
55
+ tests/test_jit_start_optimize_workaround.py
56
+ tests/test_lastindex_ovector_fastpath.py
57
+ tests/test_lastindex_semantics.py
58
+ tests/test_literal_capture_findall_fastpath.py
59
+ tests/test_literal_capture_split_fastpath.py
47
60
  tests/test_memory.py
61
+ tests/test_mmap.py
48
62
  tests/test_module.py
63
+ tests/test_multi_literal_capture_findall_fastpath.py
64
+ tests/test_multi_literal_capture_split_fastpath.py
49
65
  tests/test_pattern.py
66
+ tests/test_python_coverage_audit.py
50
67
  tests/test_re_compat.py
51
68
  tests/test_setup_utils_multiarch.py
52
69
  tests/test_simd.py
70
+ tests/test_stdlib_flag_fastpath.py
71
+ tests/test_sub_count_one_fastpath.py
72
+ tests/test_sub_reference_fastpath.py
73
+ tests/test_template_dispatch_fastpath.py
53
74
  tests/test_threaded_backend.py
54
75
  tests/test_threads.py
55
76
  tests/test_transformers_regex.py
@@ -24,6 +24,9 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
24
24
 
25
25
 
26
26
  ## Latest News 🚀
27
+ * 08/09–08/10/2026 **API performance and safety update**: common regex workloads and ordered `parallel_map(search)` workloads were benchmarked across Python 3.10 and free-threaded Python 3.14t/GIL=0. Compatibility fallbacks preserve complex patterns, subclasses, buffers, and callables. Unsafe UTF bytes compilation is also blocked, with differential, randomized, concurrency, subprocess, and memory-safety coverage. 🧵⚡🛡️
28
+ * 08/08/2026 **0.6.0**: `findall`, `finditer`, `sub`/`subn`, `split`, and `match`/`search`/`fullmatch` are now up to **46x faster** than `stdlib.re` and **48x faster** than `regex` on `finditer`/`findall` workloads, **13x** on `split`, and **2–9x** on `sub`/`subn` backref workloads, with full `re` semantics. Free-threaded `findall` reaches **13.8x** vs `re` on 8 threads. 🚀⚡
29
+ * 07/27/2026 [0.5.0](https://github.com/ModelCloud/PyPcre/releases/tag/v0.5.0): Zero-copy buffer-protocol subject support (`mmap.mmap`, `bytearray`, `array.array`) with UTF-8 validation and GIL=0-safe memory pinning. 🗂️⚡
27
30
  * 07/24/2026 [0.4.0](https://github.com/ModelCloud/PyPcre/releases/tag/v0.4.0): C extension hardening (memory/pointer safety, bounds checks, atomic allocator init), GIL=0 safety verified, vectorized UTF-8 index/offset conversion, GIL-release threshold for small calls, C `findall` implementation, and README competitor benchmarks. 🛡️⚡
28
31
  * 04/13/2026 [0.3.0](https://github.com/ModelCloud/PyPcre/releases/tag/v0.3.0): Lower-overhead public `Match` objects, faster hot-path `match()` / `search()` / `fullmatch()` / `findall()`, and tighter free-threaded execution. ⚡
29
32
  * 03/22/2026 [0.2.15](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.15): Python 3.15 `re` compatibility (`prefixmatch`, `NOFLAG`) ✅
@@ -43,10 +46,10 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
43
46
 
44
47
  - 🧬 **Full power of PCRE2**: PyPcre uses the real `PCRE2` engine, so you get native compile options, semantics, JIT, and upstream tuning.
45
48
  - 🔥 **More expressive regex syntax**: `PCRE2` supports constructs beyond stdlib `re`, including atomic groups `(?>...)`, possessive quantifiers `++`, branch-reset groups `(?|...)`, richer lookarounds, and backtracking control verbs like `(*SKIP)(*FAIL)`.
46
- - 🧵 **Thread-safe into `nogil`**: PyPcre is built for `PYTHON_GIL=0`, with CI coverage, lock-aware caches, reusable match/JIT resources, and `parallel_map()` for multi-subject fan-out.
49
+ - 🧵 **Thread-safe into `nogil`**: PyPcre is built for `PYTHON_GIL=0`, with CI coverage and `parallel_map()` for multi-subject fan-out.
47
50
  - ⚡ **Fast on real workloads**: `PCRE2` JIT plus cached compiled patterns lets PyPcre match or beat `re` and `regex` on many common scans, especially multiline searches, lookaround-heavy patterns, and free-threaded execution.
48
51
  - 🛡️ **Safer operational story**: PyPcre prefers the system `libpcre2-8` shared library so normal OS package updates can bring security and bug-fix benefits without a bundled fork.
49
- - ✅ **Validated thoroughly**: the project runs API tests, fuzz tests, memory-safety checks, local `valgrind` leak checks, and `massif` heap profiles. Recent local profiling found `0` definite leaks and `0` possible leaks in both the public API and raw binding paths.
52
+ - ✅ **Validated thoroughly**: the project runs API, fuzz, concurrency, and memory-safety tests.
50
53
 
51
54
  ### Quick Comparison 🥊
52
55
 
@@ -63,17 +66,69 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
63
66
 
64
67
  ### Benchmark Highlights 🏁
65
68
 
66
- Measured on a `Python 3.14.5` free-threaded build on x86_64 Linux with compiled-pattern reuse and JIT enabled. Times are the best of several runs; lower is better. Only workloads where PyPcre is decisively faster than both `stdlib.re` and `regex` are shown.
67
-
68
- A reproducible version of this benchmark lives in [`benchmarks/competitor_bench.py`](benchmarks/competitor_bench.py).
69
+ The tables below summarize representative public workloads. Lower is better.
70
+
71
+ #### Fan-out and API speedups
72
+
73
+ Both interpreter runs used the same scheduler policy. The host reports 12
74
+ performance logical CPUs and 4 efficiency logical CPUs; macOS does not provide
75
+ an unprivileged hard per-process CPU mask, so these measurements record the
76
+ topology rather than claiming hard CPU affinity.
77
+
78
+ | Workload | Python 3.10 | Python 3.14t/GIL=0 |
79
+ | --- | ---: | ---: |
80
+ | `parallel_map(search)`, 16 × 1 MiB subjects, 12 workers | **8.57x** | **7.85x** |
81
+ | `parallel_map(findall)`, 48 × 1 MiB subjects, 12 workers | **11.51x** | **11.25x** |
82
+ | No-op `escape("literal")` | **5.0x** | **3.6x** |
83
+ | No-op `escape(b"literal")` | **6.9x** | **6.5x** |
84
+ | Bound literal `sub(..., count=1)` | **4.7x** | **4.3x** |
85
+ | Bound numeric-reference `sub(..., count=1)` | **4.3x** | **4.1x** |
86
+ | Bound explicit-reference `sub(..., count=1)` | **4.6x** | **4.3x** |
87
+ | Bound named-reference `sub(..., count=1)` | **4.2x** | **4.3x** |
88
+ | Bound numeric-reference `sub(..., count=2)` | **4.6x** | **4.2x** |
89
+ | Bound numeric-reference `sub(..., count=4)` | **6.0x** | **5.1x** |
90
+ | Bound numeric-reference `sub(..., count=8)` | **8.6x** | **6.1x** |
91
+ | Cached compile with `re.I` | **6.7x** | **6.9x** |
92
+ | Cached compile with `re.I|re.M|re.S|re.X` | **6.2x** | **6.4x** |
93
+ | First-read `lastindex` cost, sole capture | **11.8x** | **8.2x** |
94
+ | Deprecated `template()` compatibility call | **6.2x** | **1.1x** |
95
+ | Literal-capture `findall`, 100 matches | **5.0x** | **6.2x** |
96
+ | Literal-capture `findall`, 500 matches | **7.0x** | **8.4x** |
97
+ | Two literal captures `findall`, 100 matches | **7.5x** | **10.8x** |
98
+ | Two literal captures `findall`, 500 matches | **13.5x** | **18.3x** |
99
+ | Eight literal captures `findall`, 500 matches | **24.5x** | **29.4x** |
100
+ | Three literal captures `split`, 500 captures | **4.5x** | **6.5x** |
101
+ | Eight literal captures `split`, 500 captures | **6.6x** | **9.0x** |
102
+ | Literal-capture `split`, 100 captures | **2.6x** | **3.5x** |
103
+ | Literal-capture `split`, 2,000 captures | **3.1x** | **3.8x** |
104
+ | Bound one-character literal `Pattern.split` | **2.1x** | **1.7x** |
105
+ | Bound backreference `sub` hot path | **1.38 μs** | **1.14 μs** |
106
+ | One-match numeric-reference `Pattern.sub` | **0.45 μs** | **0.34 μs** |
107
+ | One-match explicit-reference `Pattern.sub` | **0.45 μs** | **0.31 μs** |
108
+ | One-match named-reference `Pattern.sub` | **0.46 μs** | **0.33 μs** |
109
+ | Repeated call-local `Match.groups()` | **~0.05 μs** | **~0.05 μs** |
110
+ | Call-local `Match.expand(r"[\\1]")` | **0.07 μs** | **0.07 μs** |
111
+ | Call-local `Match.expand(r"[\\g<1>]")` | **0.11 μs** | **0.08 μs** |
112
+ | Call-local `Match.expand(r"[\\g<word>]")` | **0.13 μs** | **0.11 μs** |
113
+ | Call-local two-name `Match.expand` | **0.18 μs** | **0.15 μs** |
114
+ | Call-local three-name `Match.expand` | **0.23 μs** | **0.20 μs** |
115
+ | Call-local eight-name `Match.expand` | **0.44 μs** | **0.39 μs** |
116
+ | Literal-backslash + named `Match.expand` | **0.12 μs** | **0.10 μs** |
117
+ | Named + backslash-suffix `Match.expand` | **0.16 μs** | **0.13 μs** |
118
+ | Repeated default `compile("(x)")` | **0.49 μs** | **0.38 μs** |
119
+ | Repeated integer-flagged `compile("x", CASELESS)` | **1.16 μs** | **0.81 μs** |
120
+
121
+ The parallel figures are serial-to-parallel speedups and preserve input order
122
+ and exception behavior. They were measured with the same scheduler policy for
123
+ both interpreters on the Apple host.
69
124
 
70
125
  #### `findall` — large multiline and lookaround workloads
71
126
 
72
127
  | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
73
128
  | --- | ---: | ---: | ---: | --- |
74
- | Extract `WARN` / `ERROR` lines (multiline) | `0.60` | `29.35` | `30.85` | **49x** vs `re`, **52x** vs `regex` |
75
- | Per-line full-name extraction (multiline) | `1.02` | `29.50` | `15.95` | **29x** vs `re`, **16x** vs `regex` |
76
- | Lookbehind + negative-lookahead tokens | `3.78` | `11.81` | `10.42` | **3.1x** vs `re`, **2.8x** vs `regex` |
129
+ | Extract `WARN` / `ERROR` lines (multiline) | `0.471` | `15.098` | `16.281` | **32.1x** vs `re`, **34.6x** vs `regex` |
130
+ | Per-line full-name extraction (multiline) | `0.588` | `14.305` | `8.000` | **24.3x** vs `re`, **13.6x** vs `regex` |
131
+ | Lookbehind + negative-lookahead tokens | `1.013` | `6.129` | `5.267` | **6.1x** vs `re`, **5.2x** vs `regex` |
77
132
 
78
133
  Patterns used:
79
134
 
@@ -88,19 +143,51 @@ Patterns used:
88
143
 
89
144
  #### `finditer` — same workloads
90
145
 
146
+ Measured on Python 3.10.11 arm64 with compiled-pattern reuse and JIT enabled. A reproducible version lives in [`benchmarks/finditer_bench.py`](benchmarks/finditer_bench.py).
147
+
148
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
149
+ | --- | ---: | ---: | ---: | --- |
150
+ | Extract `WARN` / `ERROR` lines | `0.449` | `17.145` | `16.175` | **38.2x** vs `re`, **36.0x** vs `regex` |
151
+ | Per-line full-name extraction | `0.564` | `16.258` | `7.874` | **28.8x** vs `re`, **14.0x** vs `regex` |
152
+ | Lookbehind + negative lookahead | `1.942` | `8.548` | `6.037` | **4.4x** vs `re`, **3.1x** vs `regex` |
153
+
154
+ #### `sub` / `subn` — high-volume replacement workloads
155
+
156
+ Measured on Python 3.10.11 arm64 with compiled-pattern reuse and JIT enabled. Values are medians of three outer runs; lower is better. The benchmark replaces 100,000 space-separated tokens.
157
+
158
+ A reproducible version lives in [`benchmarks/sub_bench.py`](benchmarks/sub_bench.py).
159
+
91
160
  | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
92
161
  | --- | ---: | ---: | ---: | --- |
93
- | Extract `WARN` / `ERROR` lines | `0.60` | `29.33` | `31.23` | **49x** vs `re`, **52x** vs `regex` |
94
- | Per-line full-name extraction | `1.02` | `30.09` | `16.25` | **29x** vs `re`, **16x** vs `regex` |
162
+ | Literal replacement (`\w+` → `[X]`) | `3.127` | `6.489` | `9.992` | **2.1x** vs `re`, **3.2x** vs `regex` |
163
+ | Single numeric backref (`(w)\d+` → `[\1]`) | `4.003` | `30.098` | `12.481` | **7.5x** vs `re`, **3.1x** vs `regex` |
164
+ | Two numeric backrefs (`(w)(\d+)` → `\2-\1`) | `9.940` | `35.052` | `16.201` | **3.5x** vs `re`, **1.6x** vs `regex` |
165
+ | Named backref (`(?P<g>\w+)` → `<\g<g>>`) | `9.765` | `32.434` | `15.216` | **3.3x** vs `re`, **1.6x** vs `regex` |
166
+
167
+ #### `split` — high-volume delimiter workloads
168
+
169
+ Measured on Python 3.10.11 arm64 with compiled-pattern reuse and JIT enabled. Values are medians of three outer runs; lower is better. The benchmark splits 100,000 space-separated tokens.
170
+
171
+ A reproducible version lives in [`benchmarks/split_bench.py`](benchmarks/split_bench.py).
172
+
173
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
174
+ | --- | ---: | ---: | ---: | --- |
175
+ | Delimiter no group (`\s+`) | `3.349` | `8.013` | `9.913` | **2.4x** vs `re`, **3.0x** vs `regex` |
176
+ | Delimiter with group (`(\s+)`) | `5.080` | `9.142` | `12.374` | **1.8x** vs `re`, **2.4x** vs `regex` |
177
+ | Single char (` `) | `1.484` | `1.666` | `6.963` | **1.1x** vs `re`, **4.7x** vs `regex` |
178
+ | Single char with group (`( )`) | `1.772` | `4.203` | `8.875` | **2.4x** vs `re`, **5.0x** vs `regex` |
179
+ | Empty pattern (`''`) | `35.736` | `18.386` | `35.801` | **0.5x** vs `re`, parity vs `regex` |
95
180
 
96
181
  ### Free-Threaded Benchmark Highlights 🧵
97
182
 
98
- Measured in the same environment with `8` threads fanning out over independent copies of each workload. Times are the best of several runs; lower is better.
183
+ Measured on the same Apple arm64 `Python 3.14.0rc2` free-threaded build with `8` threads fanning out over split copies of each workload. Values are medians of three outer runs; lower is better.
184
+
185
+ A reproducible version lives in [`benchmarks/free_threaded_bench.py`](benchmarks/free_threaded_bench.py).
99
186
 
100
187
  | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
101
188
  | --- | ---: | ---: | ---: | --- |
102
- | Extract `WARN` / `ERROR` lines (`findall`) | `3.26` | `32.57` | `34.41` | **10.0x** vs `re`, **10.6x** vs `regex` |
103
- | Per-line full-name extraction (`findall`) | `3.18` | `31.84` | `17.31` | **10.0x** vs `re`, **5.4x** vs `regex` |
189
+ | Extract `WARN` / `ERROR` lines (`findall`) | `0.401` | `2.430` | `2.626` | **6.1x** vs `re`, **6.6x** vs `regex` |
190
+ | Per-line full-name extraction (`findall`) | `0.462` | `2.351` | `1.477` | **5.1x** vs `re`, **3.2x** vs `regex` |
104
191
 
105
192
  PyPcre is the stronger all-around choice when you want more than the baseline: full `PCRE2` features, more expressive syntax, JIT, explicit free-threaded support, and a stable `re`-compatible API surface. It keeps Python ergonomics while giving you a substantially more capable engine. 🚀
106
193
 
@@ -246,7 +333,13 @@ the conversion without repeating the flag.
246
333
  - `pcre.configure(jit=False)` disables JIT globally. `Flag.JIT` and
247
334
  `Flag.NO_JIT` let you override that per pattern.
248
335
  - `pcre.set_cache_limit()`, `pcre.get_cache_limit()`, and `pcre.clear_cache()`
249
- control the high-level compile cache.
336
+ control every high-level compile/template helper cache in the active context.
337
+ A zero limit disables them, and `None` uses a 256-entry hard safety ceiling
338
+ rather than permitting unbounded growth. High-level cache entries never cross
339
+ thread scope in the default thread-local strategy; a clear invalidates live
340
+ workers' high-level helper caches on their next cache-backed call. Backend
341
+ scratch buffers remain thread-scoped and are released when that thread exits.
342
+ Oversized patterns and templates are not retained.
250
343
  - `pcre.configure_threads()`, `pcre.configure_thread_pool()`,
251
344
  `shutdown_thread_pool()`, `Flag.THREADS`, and `Flag.NO_THREADS` are available
252
345
  if you want to opt into or restrict threaded execution.