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.
- {pypcre-0.4.0 → pypcre-0.6.1}/PKG-INFO +110 -17
- {pypcre-0.4.0 → pypcre-0.6.1}/PyPcre.egg-info/PKG-INFO +110 -17
- {pypcre-0.4.0 → pypcre-0.6.1}/PyPcre.egg-info/SOURCES.txt +21 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/README.md +107 -14
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre/__init__.py +3 -8
- pypcre-0.6.1/pcre/_stdlib_re.py +37 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre/cache.py +111 -25
- pypcre-0.6.1/pcre/pcre.py +1930 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre/re_compat.py +171 -11
- pypcre-0.6.1/pcre/threads.py +292 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/atomic_compat.h +36 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/cache.c +217 -80
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/error.c +21 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/flag.c +8 -4
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/memory.c +14 -20
- pypcre-0.6.1/pcre_ext/pattern_cache.c +431 -0
- pypcre-0.6.1/pcre_ext/pcre2.c +6477 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/pcre2_module.h +20 -1
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/string_helpers.c +255 -34
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/util.c +107 -11
- {pypcre-0.4.0 → pypcre-0.6.1}/pyproject.toml +4 -4
- {pypcre-0.4.0 → pypcre-0.6.1}/setup.py +7 -1
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_api_parity.py +176 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_bytes.py +6 -8
- pypcre-0.6.1/tests/test_c_api_audit.py +440 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_cache.py +86 -24
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_cache_global.py +32 -3
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_cache_global_inproc.py +58 -5
- pypcre-0.6.1/tests/test_cache_scope_safety.py +276 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_clobber.py +4 -1
- pypcre-0.6.1/tests/test_clobber_verify.py +836 -0
- pypcre-0.6.1/tests/test_compile_utf_no_check_safety.py +150 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_core.py +2 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_coverage_gaps.py +57 -2
- pypcre-0.6.1/tests/test_escape_fastpath.py +83 -0
- pypcre-0.6.1/tests/test_expand_fastpath.py +302 -0
- pypcre-0.6.1/tests/test_gil_zero_safety.py +245 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_jit.py +7 -2
- pypcre-0.6.1/tests/test_jit_anchoring.py +166 -0
- pypcre-0.6.1/tests/test_jit_start_optimize_workaround.py +82 -0
- pypcre-0.6.1/tests/test_lastindex_ovector_fastpath.py +78 -0
- pypcre-0.6.1/tests/test_lastindex_semantics.py +115 -0
- pypcre-0.6.1/tests/test_literal_capture_findall_fastpath.py +123 -0
- pypcre-0.6.1/tests/test_literal_capture_split_fastpath.py +94 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_memory.py +66 -8
- pypcre-0.6.1/tests/test_mmap.py +139 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_module.py +5 -0
- pypcre-0.6.1/tests/test_multi_literal_capture_findall_fastpath.py +144 -0
- pypcre-0.6.1/tests/test_multi_literal_capture_split_fastpath.py +114 -0
- pypcre-0.6.1/tests/test_python_coverage_audit.py +825 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_re_compat.py +2 -3
- pypcre-0.6.1/tests/test_stdlib_flag_fastpath.py +111 -0
- pypcre-0.6.1/tests/test_sub_count_one_fastpath.py +154 -0
- pypcre-0.6.1/tests/test_sub_reference_fastpath.py +140 -0
- pypcre-0.6.1/tests/test_template_dispatch_fastpath.py +74 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_threaded_backend.py +26 -4
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_threads.py +126 -10
- pypcre-0.4.0/pcre/_stdlib_re.py +0 -28
- pypcre-0.4.0/pcre/pcre.py +0 -820
- pypcre-0.4.0/pcre/threads.py +0 -181
- pypcre-0.4.0/pcre_ext/pattern_cache.c +0 -325
- pypcre-0.4.0/pcre_ext/pcre2.c +0 -2662
- {pypcre-0.4.0 → pypcre-0.6.1}/LICENSE +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/MANIFEST.in +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/PyPcre.egg-info/dependency_links.txt +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/PyPcre.egg-info/top_level.txt +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre/error.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre/flags.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/jit_support.c +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/pcre_ext/pcre2.h +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/setup.cfg +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/setup_utils.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_accuracy.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_basic.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_bench_string.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_benchmark.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_clobber_thread.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_errors.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_flags.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_pattern.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_setup_utils_multiarch.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_simd.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_transformers_regex.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.1}/tests/test_utf8.py +0 -0
- {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.
|
|
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/
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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.
|
|
98
|
-
| Per-line full-name extraction (multiline) | `
|
|
99
|
-
| Lookbehind + negative-lookahead tokens | `
|
|
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
|
-
|
|
|
117
|
-
|
|
|
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
|
|
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`) | `
|
|
126
|
-
| Per-line full-name extraction (`findall`) | `
|
|
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
|
|
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.
|
|
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/
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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.
|
|
98
|
-
| Per-line full-name extraction (multiline) | `
|
|
99
|
-
| Lookbehind + negative-lookahead tokens | `
|
|
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
|
-
|
|
|
117
|
-
|
|
|
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
|
|
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`) | `
|
|
126
|
-
| Per-line full-name extraction (`findall`) | `
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
75
|
-
| Per-line full-name extraction (multiline) | `
|
|
76
|
-
| Lookbehind + negative-lookahead tokens | `
|
|
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
|
-
|
|
|
94
|
-
|
|
|
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
|
|
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`) | `
|
|
103
|
-
| Per-line full-name extraction (`findall`) | `
|
|
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
|
|
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.
|