PyPcre 0.4.0__tar.gz → 0.6.0__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.0}/PKG-INFO +44 -10
- {pypcre-0.4.0 → pypcre-0.6.0}/PyPcre.egg-info/PKG-INFO +44 -10
- {pypcre-0.4.0 → pypcre-0.6.0}/PyPcre.egg-info/SOURCES.txt +6 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/README.md +43 -9
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre/__init__.py +2 -1
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre/pcre.py +158 -54
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre/re_compat.py +17 -3
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/cache.c +165 -72
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/flag.c +8 -4
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/pattern_cache.c +95 -5
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/pcre2.c +2062 -306
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/pcre2_module.h +14 -1
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/string_helpers.c +66 -34
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/util.c +94 -3
- {pypcre-0.4.0 → pypcre-0.6.0}/pyproject.toml +1 -1
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_api_parity.py +91 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_bytes.py +6 -8
- pypcre-0.6.0/tests/test_c_api_audit.py +349 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_cache.py +4 -1
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_coverage_gaps.py +41 -0
- pypcre-0.6.0/tests/test_gil_zero_safety.py +128 -0
- pypcre-0.6.0/tests/test_jit_anchoring.py +166 -0
- pypcre-0.6.0/tests/test_lastindex_semantics.py +115 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_memory.py +66 -8
- pypcre-0.6.0/tests/test_mmap.py +139 -0
- pypcre-0.6.0/tests/test_python_coverage_audit.py +348 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_re_compat.py +2 -3
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_threads.py +4 -4
- {pypcre-0.4.0 → pypcre-0.6.0}/LICENSE +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/MANIFEST.in +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/PyPcre.egg-info/dependency_links.txt +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/PyPcre.egg-info/top_level.txt +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre/_stdlib_re.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre/cache.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre/error.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre/flags.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre/threads.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/atomic_compat.h +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/error.c +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/jit_support.c +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/memory.c +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/pcre2.h +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/setup.cfg +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/setup.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/setup_utils.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_accuracy.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_basic.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_bench_string.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_benchmark.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_cache_global.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_cache_global_inproc.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_clobber.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_clobber_thread.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_core.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_errors.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_flags.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_jit.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_module.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_pattern.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_setup_utils_multiarch.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_simd.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_threaded_backend.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_transformers_regex.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_utf8.py +0 -0
- {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPcre
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -47,6 +47,8 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
## Latest News 🚀
|
|
50
|
+
* 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. 🚀⚡
|
|
51
|
+
* 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
52
|
* 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
53
|
* 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
54
|
* 03/22/2026 [0.2.15](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.15): Python 3.15 `re` compatibility (`prefixmatch`, `NOFLAG`) ✅
|
|
@@ -86,7 +88,7 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
|
|
|
86
88
|
|
|
87
89
|
### Benchmark Highlights 🏁
|
|
88
90
|
|
|
89
|
-
Measured on a `Python 3.14.
|
|
91
|
+
Measured on a `Python 3.14.6` 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
92
|
|
|
91
93
|
A reproducible version of this benchmark lives in [`benchmarks/competitor_bench.py`](benchmarks/competitor_bench.py).
|
|
92
94
|
|
|
@@ -94,9 +96,9 @@ A reproducible version of this benchmark lives in [`benchmarks/competitor_bench.
|
|
|
94
96
|
|
|
95
97
|
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
96
98
|
| --- | ---: | ---: | ---: | --- |
|
|
97
|
-
| Extract `WARN` / `ERROR` lines (multiline) | `0.
|
|
98
|
-
| Per-line full-name extraction (multiline) | `
|
|
99
|
-
| Lookbehind + negative-lookahead tokens | `
|
|
99
|
+
| Extract `WARN` / `ERROR` lines (multiline) | `0.664` | `27.159` | `30.772` | **40.9x** vs `re`, **46.3x** vs `regex` |
|
|
100
|
+
| Per-line full-name extraction (multiline) | `0.914` | `25.685` | `14.482` | **28.1x** vs `re`, **15.8x** vs `regex` |
|
|
101
|
+
| Lookbehind + negative-lookahead tokens | `1.874` | `12.353` | `10.386` | **6.6x** vs `re`, **5.5x** vs `regex` |
|
|
100
102
|
|
|
101
103
|
Patterns used:
|
|
102
104
|
|
|
@@ -111,19 +113,51 @@ Patterns used:
|
|
|
111
113
|
|
|
112
114
|
#### `finditer` — same workloads
|
|
113
115
|
|
|
116
|
+
Measured on a Python 3.10 x86_64 Linux build with compiled-pattern reuse and JIT enabled. A reproducible version lives in [`benchmarks/finditer_bench.py`](benchmarks/finditer_bench.py).
|
|
117
|
+
|
|
118
|
+
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
119
|
+
| --- | ---: | ---: | ---: | --- |
|
|
120
|
+
| Extract `WARN` / `ERROR` lines | `0.663` | `30.747` | `31.862` | **46.4x** vs `re`, **48.0x** vs `regex` |
|
|
121
|
+
| Per-line full-name extraction | `0.919` | `29.127` | `14.103` | **31.7x** vs `re`, **15.3x** vs `regex` |
|
|
122
|
+
| Lookbehind + negative lookahead | `3.755` | `15.828` | `11.896` | **4.2x** vs `re`, **3.2x** vs `regex` |
|
|
123
|
+
|
|
124
|
+
#### `sub` / `subn` — high-volume replacement workloads
|
|
125
|
+
|
|
126
|
+
Measured on a Python 3.10 x86_64 Linux build with compiled-pattern reuse and JIT enabled. Times are the best of several runs; lower is better. The benchmark replaces 100,000 space-separated tokens.
|
|
127
|
+
|
|
128
|
+
A reproducible version lives in [`benchmarks/sub_bench.py`](benchmarks/sub_bench.py).
|
|
129
|
+
|
|
114
130
|
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
115
131
|
| --- | ---: | ---: | ---: | --- |
|
|
116
|
-
|
|
|
117
|
-
|
|
|
132
|
+
| Literal replacement (`\w+` → `[X]`) | `5.933` | `13.367` | `19.107` | **2.3x** vs `re`, **3.2x** vs `regex` |
|
|
133
|
+
| Single numeric backref (`(w)\d+` → `[\1]`) | `7.221` | `64.715` | `25.482` | **9.0x** vs `re`, **3.5x** vs `regex` |
|
|
134
|
+
| Two numeric backrefs (`(w)(\d+)` → `\2-\1`) | `17.600` | `76.222` | `32.048` | **4.3x** vs `re`, **1.8x** vs `regex` |
|
|
135
|
+
| Named backref (`(?P<g>\w+)` → `<\g<g>>`) | `14.684` | `71.053` | `30.222` | **4.8x** vs `re`, **2.1x** vs `regex` |
|
|
136
|
+
|
|
137
|
+
#### `split` — high-volume delimiter workloads
|
|
138
|
+
|
|
139
|
+
Measured on a Python 3.10 x86_64 Linux build with compiled-pattern reuse and JIT enabled. Times are the best of several runs; lower is better. The benchmark splits 100,000 space-separated tokens.
|
|
140
|
+
|
|
141
|
+
A reproducible version lives in [`benchmarks/split_bench.py`](benchmarks/split_bench.py).
|
|
142
|
+
|
|
143
|
+
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
144
|
+
| --- | ---: | ---: | ---: | --- |
|
|
145
|
+
| Delimiter no group (`\s+`) | `7.315` | `17.394` | `19.690` | **2.4x** vs `re`, **2.7x** vs `regex` |
|
|
146
|
+
| Delimiter with group (`(\s+)`) | `12.360` | `21.099` | `25.145` | **1.7x** vs `re`, **2.0x** vs `regex` |
|
|
147
|
+
| Single char (` `) | `4.583` | `3.990` | `14.577` | parity vs `re`, **3.2x** vs `regex` |
|
|
148
|
+
| Single char with group (`( )`) | `8.913` | `11.091` | `18.595` | **1.2x** vs `re`, **2.1x** vs `regex` |
|
|
149
|
+
| Empty pattern (`''`) | `5.228` | `45.552` | `69.913` | **8.7x** vs `re`, **13.4x** vs `regex` |
|
|
118
150
|
|
|
119
151
|
### Free-Threaded Benchmark Highlights 🧵
|
|
120
152
|
|
|
121
|
-
Measured
|
|
153
|
+
Measured on the same `Python 3.14.6` free-threaded build with `8` threads fanning out over split copies of each workload. Times are the best of several runs; lower is better.
|
|
154
|
+
|
|
155
|
+
A reproducible version lives in [`benchmarks/free_threaded_bench.py`](benchmarks/free_threaded_bench.py).
|
|
122
156
|
|
|
123
157
|
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
124
158
|
| --- | ---: | ---: | ---: | --- |
|
|
125
|
-
| Extract `WARN` / `ERROR` lines (`findall`) | `
|
|
126
|
-
| Per-line full-name extraction (`findall`) | `
|
|
159
|
+
| Extract `WARN` / `ERROR` lines (`findall`) | `0.672` | `9.063` | `9.297` | **13.5x** vs `re`, **13.8x** vs `regex` |
|
|
160
|
+
| Per-line full-name extraction (`findall`) | `0.913` | `8.611` | `4.575` | **9.4x** vs `re`, **5.0x** vs `regex` |
|
|
127
161
|
|
|
128
162
|
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
163
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPcre
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -47,6 +47,8 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
## Latest News 🚀
|
|
50
|
+
* 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. 🚀⚡
|
|
51
|
+
* 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
52
|
* 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
53
|
* 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
54
|
* 03/22/2026 [0.2.15](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.15): Python 3.15 `re` compatibility (`prefixmatch`, `NOFLAG`) ✅
|
|
@@ -86,7 +88,7 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
|
|
|
86
88
|
|
|
87
89
|
### Benchmark Highlights 🏁
|
|
88
90
|
|
|
89
|
-
Measured on a `Python 3.14.
|
|
91
|
+
Measured on a `Python 3.14.6` 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
92
|
|
|
91
93
|
A reproducible version of this benchmark lives in [`benchmarks/competitor_bench.py`](benchmarks/competitor_bench.py).
|
|
92
94
|
|
|
@@ -94,9 +96,9 @@ A reproducible version of this benchmark lives in [`benchmarks/competitor_bench.
|
|
|
94
96
|
|
|
95
97
|
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
96
98
|
| --- | ---: | ---: | ---: | --- |
|
|
97
|
-
| Extract `WARN` / `ERROR` lines (multiline) | `0.
|
|
98
|
-
| Per-line full-name extraction (multiline) | `
|
|
99
|
-
| Lookbehind + negative-lookahead tokens | `
|
|
99
|
+
| Extract `WARN` / `ERROR` lines (multiline) | `0.664` | `27.159` | `30.772` | **40.9x** vs `re`, **46.3x** vs `regex` |
|
|
100
|
+
| Per-line full-name extraction (multiline) | `0.914` | `25.685` | `14.482` | **28.1x** vs `re`, **15.8x** vs `regex` |
|
|
101
|
+
| Lookbehind + negative-lookahead tokens | `1.874` | `12.353` | `10.386` | **6.6x** vs `re`, **5.5x** vs `regex` |
|
|
100
102
|
|
|
101
103
|
Patterns used:
|
|
102
104
|
|
|
@@ -111,19 +113,51 @@ Patterns used:
|
|
|
111
113
|
|
|
112
114
|
#### `finditer` — same workloads
|
|
113
115
|
|
|
116
|
+
Measured on a Python 3.10 x86_64 Linux build with compiled-pattern reuse and JIT enabled. A reproducible version lives in [`benchmarks/finditer_bench.py`](benchmarks/finditer_bench.py).
|
|
117
|
+
|
|
118
|
+
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
119
|
+
| --- | ---: | ---: | ---: | --- |
|
|
120
|
+
| Extract `WARN` / `ERROR` lines | `0.663` | `30.747` | `31.862` | **46.4x** vs `re`, **48.0x** vs `regex` |
|
|
121
|
+
| Per-line full-name extraction | `0.919` | `29.127` | `14.103` | **31.7x** vs `re`, **15.3x** vs `regex` |
|
|
122
|
+
| Lookbehind + negative lookahead | `3.755` | `15.828` | `11.896` | **4.2x** vs `re`, **3.2x** vs `regex` |
|
|
123
|
+
|
|
124
|
+
#### `sub` / `subn` — high-volume replacement workloads
|
|
125
|
+
|
|
126
|
+
Measured on a Python 3.10 x86_64 Linux build with compiled-pattern reuse and JIT enabled. Times are the best of several runs; lower is better. The benchmark replaces 100,000 space-separated tokens.
|
|
127
|
+
|
|
128
|
+
A reproducible version lives in [`benchmarks/sub_bench.py`](benchmarks/sub_bench.py).
|
|
129
|
+
|
|
114
130
|
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
115
131
|
| --- | ---: | ---: | ---: | --- |
|
|
116
|
-
|
|
|
117
|
-
|
|
|
132
|
+
| Literal replacement (`\w+` → `[X]`) | `5.933` | `13.367` | `19.107` | **2.3x** vs `re`, **3.2x** vs `regex` |
|
|
133
|
+
| Single numeric backref (`(w)\d+` → `[\1]`) | `7.221` | `64.715` | `25.482` | **9.0x** vs `re`, **3.5x** vs `regex` |
|
|
134
|
+
| Two numeric backrefs (`(w)(\d+)` → `\2-\1`) | `17.600` | `76.222` | `32.048` | **4.3x** vs `re`, **1.8x** vs `regex` |
|
|
135
|
+
| Named backref (`(?P<g>\w+)` → `<\g<g>>`) | `14.684` | `71.053` | `30.222` | **4.8x** vs `re`, **2.1x** vs `regex` |
|
|
136
|
+
|
|
137
|
+
#### `split` — high-volume delimiter workloads
|
|
138
|
+
|
|
139
|
+
Measured on a Python 3.10 x86_64 Linux build with compiled-pattern reuse and JIT enabled. Times are the best of several runs; lower is better. The benchmark splits 100,000 space-separated tokens.
|
|
140
|
+
|
|
141
|
+
A reproducible version lives in [`benchmarks/split_bench.py`](benchmarks/split_bench.py).
|
|
142
|
+
|
|
143
|
+
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
144
|
+
| --- | ---: | ---: | ---: | --- |
|
|
145
|
+
| Delimiter no group (`\s+`) | `7.315` | `17.394` | `19.690` | **2.4x** vs `re`, **2.7x** vs `regex` |
|
|
146
|
+
| Delimiter with group (`(\s+)`) | `12.360` | `21.099` | `25.145` | **1.7x** vs `re`, **2.0x** vs `regex` |
|
|
147
|
+
| Single char (` `) | `4.583` | `3.990` | `14.577` | parity vs `re`, **3.2x** vs `regex` |
|
|
148
|
+
| Single char with group (`( )`) | `8.913` | `11.091` | `18.595` | **1.2x** vs `re`, **2.1x** vs `regex` |
|
|
149
|
+
| Empty pattern (`''`) | `5.228` | `45.552` | `69.913` | **8.7x** vs `re`, **13.4x** vs `regex` |
|
|
118
150
|
|
|
119
151
|
### Free-Threaded Benchmark Highlights 🧵
|
|
120
152
|
|
|
121
|
-
Measured
|
|
153
|
+
Measured on the same `Python 3.14.6` free-threaded build with `8` threads fanning out over split copies of each workload. Times are the best of several runs; lower is better.
|
|
154
|
+
|
|
155
|
+
A reproducible version lives in [`benchmarks/free_threaded_bench.py`](benchmarks/free_threaded_bench.py).
|
|
122
156
|
|
|
123
157
|
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
124
158
|
| --- | ---: | ---: | ---: | --- |
|
|
125
|
-
| Extract `WARN` / `ERROR` lines (`findall`) | `
|
|
126
|
-
| Per-line full-name extraction (`findall`) | `
|
|
159
|
+
| Extract `WARN` / `ERROR` lines (`findall`) | `0.672` | `9.063` | `9.297` | **13.5x** vs `re`, **13.8x** vs `regex` |
|
|
160
|
+
| Per-line full-name extraction (`findall`) | `0.913` | `8.611` | `4.575` | **9.4x** vs `re`, **5.0x** vs `regex` |
|
|
127
161
|
|
|
128
162
|
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
163
|
|
|
@@ -34,6 +34,7 @@ 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
|
|
@@ -43,10 +44,15 @@ tests/test_core.py
|
|
|
43
44
|
tests/test_coverage_gaps.py
|
|
44
45
|
tests/test_errors.py
|
|
45
46
|
tests/test_flags.py
|
|
47
|
+
tests/test_gil_zero_safety.py
|
|
46
48
|
tests/test_jit.py
|
|
49
|
+
tests/test_jit_anchoring.py
|
|
50
|
+
tests/test_lastindex_semantics.py
|
|
47
51
|
tests/test_memory.py
|
|
52
|
+
tests/test_mmap.py
|
|
48
53
|
tests/test_module.py
|
|
49
54
|
tests/test_pattern.py
|
|
55
|
+
tests/test_python_coverage_audit.py
|
|
50
56
|
tests/test_re_compat.py
|
|
51
57
|
tests/test_setup_utils_multiarch.py
|
|
52
58
|
tests/test_simd.py
|
|
@@ -24,6 +24,8 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
## Latest News 🚀
|
|
27
|
+
* 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. 🚀⚡
|
|
28
|
+
* 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
29
|
* 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
30
|
* 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
31
|
* 03/22/2026 [0.2.15](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.15): Python 3.15 `re` compatibility (`prefixmatch`, `NOFLAG`) ✅
|
|
@@ -63,7 +65,7 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
|
|
|
63
65
|
|
|
64
66
|
### Benchmark Highlights 🏁
|
|
65
67
|
|
|
66
|
-
Measured on a `Python 3.14.
|
|
68
|
+
Measured on a `Python 3.14.6` 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
69
|
|
|
68
70
|
A reproducible version of this benchmark lives in [`benchmarks/competitor_bench.py`](benchmarks/competitor_bench.py).
|
|
69
71
|
|
|
@@ -71,9 +73,9 @@ A reproducible version of this benchmark lives in [`benchmarks/competitor_bench.
|
|
|
71
73
|
|
|
72
74
|
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
73
75
|
| --- | ---: | ---: | ---: | --- |
|
|
74
|
-
| Extract `WARN` / `ERROR` lines (multiline) | `0.
|
|
75
|
-
| Per-line full-name extraction (multiline) | `
|
|
76
|
-
| Lookbehind + negative-lookahead tokens | `
|
|
76
|
+
| Extract `WARN` / `ERROR` lines (multiline) | `0.664` | `27.159` | `30.772` | **40.9x** vs `re`, **46.3x** vs `regex` |
|
|
77
|
+
| Per-line full-name extraction (multiline) | `0.914` | `25.685` | `14.482` | **28.1x** vs `re`, **15.8x** vs `regex` |
|
|
78
|
+
| Lookbehind + negative-lookahead tokens | `1.874` | `12.353` | `10.386` | **6.6x** vs `re`, **5.5x** vs `regex` |
|
|
77
79
|
|
|
78
80
|
Patterns used:
|
|
79
81
|
|
|
@@ -88,19 +90,51 @@ Patterns used:
|
|
|
88
90
|
|
|
89
91
|
#### `finditer` — same workloads
|
|
90
92
|
|
|
93
|
+
Measured on a Python 3.10 x86_64 Linux build with compiled-pattern reuse and JIT enabled. A reproducible version lives in [`benchmarks/finditer_bench.py`](benchmarks/finditer_bench.py).
|
|
94
|
+
|
|
95
|
+
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
96
|
+
| --- | ---: | ---: | ---: | --- |
|
|
97
|
+
| Extract `WARN` / `ERROR` lines | `0.663` | `30.747` | `31.862` | **46.4x** vs `re`, **48.0x** vs `regex` |
|
|
98
|
+
| Per-line full-name extraction | `0.919` | `29.127` | `14.103` | **31.7x** vs `re`, **15.3x** vs `regex` |
|
|
99
|
+
| Lookbehind + negative lookahead | `3.755` | `15.828` | `11.896` | **4.2x** vs `re`, **3.2x** vs `regex` |
|
|
100
|
+
|
|
101
|
+
#### `sub` / `subn` — high-volume replacement workloads
|
|
102
|
+
|
|
103
|
+
Measured on a Python 3.10 x86_64 Linux build with compiled-pattern reuse and JIT enabled. Times are the best of several runs; lower is better. The benchmark replaces 100,000 space-separated tokens.
|
|
104
|
+
|
|
105
|
+
A reproducible version lives in [`benchmarks/sub_bench.py`](benchmarks/sub_bench.py).
|
|
106
|
+
|
|
91
107
|
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
92
108
|
| --- | ---: | ---: | ---: | --- |
|
|
93
|
-
|
|
|
94
|
-
|
|
|
109
|
+
| Literal replacement (`\w+` → `[X]`) | `5.933` | `13.367` | `19.107` | **2.3x** vs `re`, **3.2x** vs `regex` |
|
|
110
|
+
| Single numeric backref (`(w)\d+` → `[\1]`) | `7.221` | `64.715` | `25.482` | **9.0x** vs `re`, **3.5x** vs `regex` |
|
|
111
|
+
| Two numeric backrefs (`(w)(\d+)` → `\2-\1`) | `17.600` | `76.222` | `32.048` | **4.3x** vs `re`, **1.8x** vs `regex` |
|
|
112
|
+
| Named backref (`(?P<g>\w+)` → `<\g<g>>`) | `14.684` | `71.053` | `30.222` | **4.8x** vs `re`, **2.1x** vs `regex` |
|
|
113
|
+
|
|
114
|
+
#### `split` — high-volume delimiter workloads
|
|
115
|
+
|
|
116
|
+
Measured on a Python 3.10 x86_64 Linux build with compiled-pattern reuse and JIT enabled. Times are the best of several runs; lower is better. The benchmark splits 100,000 space-separated tokens.
|
|
117
|
+
|
|
118
|
+
A reproducible version lives in [`benchmarks/split_bench.py`](benchmarks/split_bench.py).
|
|
119
|
+
|
|
120
|
+
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
121
|
+
| --- | ---: | ---: | ---: | --- |
|
|
122
|
+
| Delimiter no group (`\s+`) | `7.315` | `17.394` | `19.690` | **2.4x** vs `re`, **2.7x** vs `regex` |
|
|
123
|
+
| Delimiter with group (`(\s+)`) | `12.360` | `21.099` | `25.145` | **1.7x** vs `re`, **2.0x** vs `regex` |
|
|
124
|
+
| Single char (` `) | `4.583` | `3.990` | `14.577` | parity vs `re`, **3.2x** vs `regex` |
|
|
125
|
+
| Single char with group (`( )`) | `8.913` | `11.091` | `18.595` | **1.2x** vs `re`, **2.1x** vs `regex` |
|
|
126
|
+
| Empty pattern (`''`) | `5.228` | `45.552` | `69.913` | **8.7x** vs `re`, **13.4x** vs `regex` |
|
|
95
127
|
|
|
96
128
|
### Free-Threaded Benchmark Highlights 🧵
|
|
97
129
|
|
|
98
|
-
Measured
|
|
130
|
+
Measured on the same `Python 3.14.6` free-threaded build with `8` threads fanning out over split copies of each workload. Times are the best of several runs; lower is better.
|
|
131
|
+
|
|
132
|
+
A reproducible version lives in [`benchmarks/free_threaded_bench.py`](benchmarks/free_threaded_bench.py).
|
|
99
133
|
|
|
100
134
|
| Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
|
|
101
135
|
| --- | ---: | ---: | ---: | --- |
|
|
102
|
-
| Extract `WARN` / `ERROR` lines (`findall`) | `
|
|
103
|
-
| Per-line full-name extraction (`findall`) | `
|
|
136
|
+
| Extract `WARN` / `ERROR` lines (`findall`) | `0.672` | `9.063` | `9.297` | **13.5x** vs `re`, **13.8x** vs `regex` |
|
|
137
|
+
| Per-line full-name extraction (`findall`) | `0.913` | `8.611` | `4.575` | **9.4x** vs `re`, **5.0x** vs `regex` |
|
|
104
138
|
|
|
105
139
|
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
140
|
|
|
@@ -12,12 +12,12 @@ surfacing PCRE2-specific flags and behaviours.
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import importlib as _importlib
|
|
15
16
|
import re as _std_re
|
|
16
17
|
from typing import Any
|
|
17
18
|
|
|
18
19
|
import pcre_ext_c as _backend
|
|
19
20
|
|
|
20
|
-
from . import error as _error_module
|
|
21
21
|
from .cache import cache_strategy, get_cache_limit, set_cache_limit
|
|
22
22
|
from .error import ERRORS_BY_CODE, ERRORS_BY_MACRO, PcreErrorCode
|
|
23
23
|
from .flags import Flag
|
|
@@ -43,6 +43,7 @@ from .pcre import (
|
|
|
43
43
|
from .threads import configure_thread_pool, configure_threads, shutdown_thread_pool
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
_error_module = _importlib.import_module(".error", __name__)
|
|
46
47
|
pcre_ext_c = _backend
|
|
47
48
|
|
|
48
49
|
__version__ = getattr(_backend, "__version__", "0.0")
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import re as _std_re
|
|
11
|
-
from collections.abc import
|
|
11
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
12
12
|
from typing import Any, List
|
|
13
13
|
|
|
14
14
|
import pcre_ext_c as _pcre2
|
|
@@ -175,13 +175,60 @@ def _normalise_flags(flags: FlagInput) -> int:
|
|
|
175
175
|
raise TypeError("flags must be an int, stdlib re flag, or an iterable of those")
|
|
176
176
|
|
|
177
177
|
|
|
178
|
+
def _pcre2_replacement_from_parsed(parsed: Any, is_bytes: bool) -> Any:
|
|
179
|
+
"""Convert a parsed Python replacement template to a PCRE2 replacement string."""
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
isinstance(parsed, tuple)
|
|
183
|
+
and len(parsed) == 2
|
|
184
|
+
and isinstance(parsed[0], list)
|
|
185
|
+
and isinstance(parsed[1], list)
|
|
186
|
+
):
|
|
187
|
+
group_slots, literals = parsed
|
|
188
|
+
slot_to_group = {slot: group for slot, group in group_slots}
|
|
189
|
+
if is_bytes:
|
|
190
|
+
parts = []
|
|
191
|
+
for i, lit in enumerate(literals):
|
|
192
|
+
if i in slot_to_group:
|
|
193
|
+
parts.append(("\\g<" + str(slot_to_group[i]) + ">").encode("ascii"))
|
|
194
|
+
if lit is not None:
|
|
195
|
+
parts.append(lit.replace(b"\\", b"\\\\").replace(b"$", b"$$"))
|
|
196
|
+
return b"".join(parts)
|
|
197
|
+
|
|
198
|
+
parts = []
|
|
199
|
+
for i, lit in enumerate(literals):
|
|
200
|
+
if i in slot_to_group:
|
|
201
|
+
parts.append("\\g<" + str(slot_to_group[i]) + ">")
|
|
202
|
+
if lit is not None:
|
|
203
|
+
parts.append(lit.replace("\\", "\\\\").replace("$", "$$"))
|
|
204
|
+
return "".join(parts)
|
|
205
|
+
|
|
206
|
+
if is_bytes:
|
|
207
|
+
parts = []
|
|
208
|
+
for item in parsed:
|
|
209
|
+
if isinstance(item, int):
|
|
210
|
+
parts.append(("\\g<" + str(item) + ">").encode("ascii"))
|
|
211
|
+
else:
|
|
212
|
+
parts.append(item.replace(b"\\", b"\\\\").replace(b"$", b"$$"))
|
|
213
|
+
return b"".join(parts)
|
|
214
|
+
|
|
215
|
+
parts = []
|
|
216
|
+
for item in parsed:
|
|
217
|
+
if isinstance(item, int):
|
|
218
|
+
parts.append("\\g<" + str(item) + ">")
|
|
219
|
+
else:
|
|
220
|
+
parts.append(item.replace("\\", "\\\\").replace("$", "$$"))
|
|
221
|
+
return "".join(parts)
|
|
222
|
+
|
|
223
|
+
|
|
178
224
|
class Pattern:
|
|
179
225
|
"""High-level wrapper around the C-backed :class:`pcre_ext_c.Pattern`."""
|
|
180
226
|
|
|
181
|
-
__slots__ = ("_pattern", "_groups_hint", "_thread_mode")
|
|
227
|
+
__slots__ = ("_pattern", "_groups_hint", "_thread_mode", "_is_c_pattern")
|
|
182
228
|
|
|
183
229
|
def __init__(self, pattern: _CPattern) -> None:
|
|
184
230
|
self._pattern = pattern
|
|
231
|
+
self._is_c_pattern = isinstance(pattern, _CPattern)
|
|
185
232
|
self._thread_mode = _THREAD_MODE_DISABLED
|
|
186
233
|
try:
|
|
187
234
|
self._groups_hint = pattern.capture_count
|
|
@@ -196,7 +243,7 @@ class Pattern:
|
|
|
196
243
|
return self._pattern.pattern
|
|
197
244
|
|
|
198
245
|
@property
|
|
199
|
-
def groupindex(self) ->
|
|
246
|
+
def groupindex(self) -> Mapping[str, int]:
|
|
200
247
|
return self._pattern.groupindex
|
|
201
248
|
|
|
202
249
|
@property
|
|
@@ -260,20 +307,17 @@ class Pattern:
|
|
|
260
307
|
) -> Match | None:
|
|
261
308
|
if type(subject) is memoryview:
|
|
262
309
|
subject = subject.tobytes()
|
|
310
|
+
if self._is_c_pattern:
|
|
311
|
+
compiled_end = resolve_endpos(subject, endpos) if endpos is not None else -1
|
|
312
|
+
return self._pattern.match(subject, pos, compiled_end, options, self)
|
|
263
313
|
if endpos is None:
|
|
264
|
-
raw = self._pattern.match(subject, pos=pos, options=options)
|
|
265
|
-
if raw is None:
|
|
266
|
-
return None
|
|
267
|
-
if _can_attach_match(raw):
|
|
268
|
-
return _ATTACH_MATCH(raw, self)
|
|
269
314
|
resolved_end = len(subject)
|
|
315
|
+
raw = self._pattern.match(subject, pos=pos, options=options)
|
|
270
316
|
else:
|
|
271
317
|
resolved_end = resolve_endpos(subject, endpos)
|
|
272
318
|
raw = self._pattern.match(subject, pos=pos, endpos=resolved_end, options=options)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if _can_attach_match(raw):
|
|
276
|
-
return _ATTACH_MATCH(raw, self)
|
|
319
|
+
if raw is None:
|
|
320
|
+
return None
|
|
277
321
|
return self._wrap_match(raw, subject, pos, resolved_end)
|
|
278
322
|
|
|
279
323
|
prefixmatch = match
|
|
@@ -288,20 +332,17 @@ class Pattern:
|
|
|
288
332
|
) -> Match | None:
|
|
289
333
|
if type(subject) is memoryview:
|
|
290
334
|
subject = subject.tobytes()
|
|
335
|
+
if self._is_c_pattern:
|
|
336
|
+
compiled_end = resolve_endpos(subject, endpos) if endpos is not None else -1
|
|
337
|
+
return self._pattern.search(subject, pos, compiled_end, options, self)
|
|
291
338
|
if endpos is None:
|
|
292
|
-
raw = self._pattern.search(subject, pos=pos, options=options)
|
|
293
|
-
if raw is None:
|
|
294
|
-
return None
|
|
295
|
-
if _can_attach_match(raw):
|
|
296
|
-
return _ATTACH_MATCH(raw, self)
|
|
297
339
|
resolved_end = len(subject)
|
|
340
|
+
raw = self._pattern.search(subject, pos=pos, options=options)
|
|
298
341
|
else:
|
|
299
342
|
resolved_end = resolve_endpos(subject, endpos)
|
|
300
343
|
raw = self._pattern.search(subject, pos=pos, endpos=resolved_end, options=options)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if _can_attach_match(raw):
|
|
304
|
-
return _ATTACH_MATCH(raw, self)
|
|
344
|
+
if raw is None:
|
|
345
|
+
return None
|
|
305
346
|
return self._wrap_match(raw, subject, pos, resolved_end)
|
|
306
347
|
|
|
307
348
|
def fullmatch(
|
|
@@ -314,20 +355,17 @@ class Pattern:
|
|
|
314
355
|
) -> Match | None:
|
|
315
356
|
if type(subject) is memoryview:
|
|
316
357
|
subject = subject.tobytes()
|
|
358
|
+
if self._is_c_pattern:
|
|
359
|
+
compiled_end = resolve_endpos(subject, endpos) if endpos is not None else -1
|
|
360
|
+
return self._pattern.fullmatch(subject, pos, compiled_end, options, self)
|
|
317
361
|
if endpos is None:
|
|
318
|
-
raw = self._pattern.fullmatch(subject, pos=pos, options=options)
|
|
319
|
-
if raw is None:
|
|
320
|
-
return None
|
|
321
|
-
if _can_attach_match(raw):
|
|
322
|
-
return _ATTACH_MATCH(raw, self)
|
|
323
362
|
resolved_end = len(subject)
|
|
363
|
+
raw = self._pattern.fullmatch(subject, pos=pos, options=options)
|
|
324
364
|
else:
|
|
325
365
|
resolved_end = resolve_endpos(subject, endpos)
|
|
326
366
|
raw = self._pattern.fullmatch(subject, pos=pos, endpos=resolved_end, options=options)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if _can_attach_match(raw):
|
|
330
|
-
return _ATTACH_MATCH(raw, self)
|
|
367
|
+
if raw is None:
|
|
368
|
+
return None
|
|
331
369
|
return self._wrap_match(raw, subject, pos, resolved_end)
|
|
332
370
|
|
|
333
371
|
def finditer(
|
|
@@ -337,44 +375,71 @@ class Pattern:
|
|
|
337
375
|
pos: int = 0,
|
|
338
376
|
endpos: int | None = None,
|
|
339
377
|
options: int = 0,
|
|
340
|
-
) ->
|
|
378
|
+
) -> Iterator[Match]:
|
|
341
379
|
if type(subject) is memoryview:
|
|
342
380
|
subject = subject.tobytes()
|
|
343
|
-
origin_pos = pos
|
|
344
381
|
resolved_end = resolve_endpos(subject, endpos)
|
|
345
382
|
backend_iter = getattr(self._pattern, "finditer", None)
|
|
346
383
|
if backend_iter is not None:
|
|
347
384
|
compiled_end = resolved_end if endpos is not None else -1
|
|
385
|
+
if self._is_c_pattern:
|
|
386
|
+
# The C iterator stamps each Match with this public Pattern, so
|
|
387
|
+
# we can return it directly without per-match Python wrapping.
|
|
388
|
+
return backend_iter(subject, pos, compiled_end, options, self)
|
|
389
|
+
raw_iter = None
|
|
348
390
|
try:
|
|
349
|
-
raw_iter = backend_iter(subject, pos=pos, endpos=compiled_end, options=options)
|
|
391
|
+
raw_iter = backend_iter(subject, pos=pos, endpos=compiled_end, options=options, owner=self)
|
|
350
392
|
except TypeError:
|
|
351
|
-
|
|
393
|
+
# Older extensions and test doubles may not accept `owner`.
|
|
394
|
+
try:
|
|
395
|
+
raw_iter = backend_iter(subject, pos=pos, endpos=compiled_end, options=options)
|
|
396
|
+
except TypeError:
|
|
397
|
+
raw_iter = None
|
|
352
398
|
if raw_iter is not None:
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
399
|
+
# Peek to detect whether the backend already stamped the public
|
|
400
|
+
# owner on its Match objects. If it did, we can yield from the
|
|
401
|
+
# raw iterator; otherwise wrap the raw results as before.
|
|
402
|
+
try:
|
|
403
|
+
peek = next(raw_iter)
|
|
404
|
+
except StopIteration:
|
|
405
|
+
return iter([])
|
|
406
|
+
if _can_attach_match(peek) and peek.re is self:
|
|
407
|
+
def _owned_iter():
|
|
408
|
+
yield peek
|
|
409
|
+
yield from raw_iter
|
|
410
|
+
return _owned_iter()
|
|
411
|
+
def _wrapped_iter():
|
|
412
|
+
yield self._wrap_match(peek, subject, pos, resolved_end)
|
|
413
|
+
for raw in raw_iter:
|
|
414
|
+
yield self._wrap_match(raw, subject, pos, resolved_end)
|
|
415
|
+
return _wrapped_iter()
|
|
356
416
|
|
|
357
417
|
search_end = resolved_end if endpos is not None else -1
|
|
358
418
|
current = pos
|
|
419
|
+
origin_pos = pos
|
|
359
420
|
subject_length = len(subject)
|
|
360
421
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
422
|
+
def _generator():
|
|
423
|
+
nonlocal current
|
|
424
|
+
while True:
|
|
425
|
+
raw = self._pattern.search(subject, pos=current, endpos=search_end, options=options)
|
|
426
|
+
if raw is None:
|
|
427
|
+
break
|
|
365
428
|
|
|
366
|
-
|
|
367
|
-
|
|
429
|
+
match_obj = self._wrap_match(raw, subject, origin_pos, resolved_end)
|
|
430
|
+
yield match_obj
|
|
368
431
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
432
|
+
start, end = match_obj.span()
|
|
433
|
+
next_pos = compute_next_pos(current, (start, end), endpos)
|
|
434
|
+
if next_pos <= current:
|
|
435
|
+
next_pos = current + 1
|
|
436
|
+
current = next_pos
|
|
437
|
+
if current > subject_length:
|
|
438
|
+
break
|
|
439
|
+
if endpos is not None and current >= resolved_end:
|
|
440
|
+
break
|
|
441
|
+
|
|
442
|
+
return _generator()
|
|
378
443
|
|
|
379
444
|
def findall(
|
|
380
445
|
self,
|
|
@@ -422,11 +487,31 @@ class Pattern:
|
|
|
422
487
|
|
|
423
488
|
def split(self, subject: Any, maxsplit: Any = 0) -> List[Any]:
|
|
424
489
|
subject = prepare_subject(subject)
|
|
490
|
+
limit = normalise_count(maxsplit)
|
|
491
|
+
if limit == 0:
|
|
492
|
+
subject_is_bytes = is_bytes_like(subject)
|
|
493
|
+
return [
|
|
494
|
+
coerce_subject_slice(
|
|
495
|
+
subject, 0, len(subject), is_bytes=subject_is_bytes
|
|
496
|
+
)
|
|
497
|
+
]
|
|
498
|
+
|
|
499
|
+
# Empty patterns split at every code point/byte; avoid per-match overhead.
|
|
500
|
+
if limit is None and self.pattern in ("", b""):
|
|
501
|
+
if is_bytes_like(subject):
|
|
502
|
+
return [b""] + [bytes(subject[i : i + 1]) for i in range(len(subject))] + [b""]
|
|
503
|
+
return [""] + list(subject) + [""]
|
|
504
|
+
|
|
505
|
+
backend_split = getattr(self._pattern, "split", None)
|
|
506
|
+
if backend_split is not None:
|
|
507
|
+
try:
|
|
508
|
+
return backend_split(subject, 0 if limit is None else limit)
|
|
509
|
+
except TypeError:
|
|
510
|
+
pass
|
|
511
|
+
|
|
425
512
|
subject_is_bytes = is_bytes_like(subject)
|
|
426
513
|
empty = b"" if subject_is_bytes else ""
|
|
427
514
|
parts: List[Any] = []
|
|
428
|
-
limit = normalise_count(maxsplit)
|
|
429
|
-
|
|
430
515
|
last_end = 0
|
|
431
516
|
splits_done = 0
|
|
432
517
|
|
|
@@ -472,6 +557,25 @@ class Pattern:
|
|
|
472
557
|
raise TypeError("replacement must be str when substituting on text")
|
|
473
558
|
template = repl
|
|
474
559
|
|
|
560
|
+
if limit is None:
|
|
561
|
+
backend_substitute = getattr(self._pattern, "substitute", None)
|
|
562
|
+
if backend_substitute is not None:
|
|
563
|
+
try:
|
|
564
|
+
parsed_template = _parser.parse_template(
|
|
565
|
+
template,
|
|
566
|
+
TemplatePatternStub(self.groups, self.groupindex),
|
|
567
|
+
)
|
|
568
|
+
except (ValueError, _std_re.error, IndexError) as exc:
|
|
569
|
+
raise PcreError(str(exc)) from exc
|
|
570
|
+
pcre2_repl = _pcre2_replacement_from_parsed(parsed_template, subject_is_bytes)
|
|
571
|
+
try:
|
|
572
|
+
backend_result = backend_substitute(subject, replacement=pcre2_repl, count=0)
|
|
573
|
+
except Exception:
|
|
574
|
+
backend_result = NotImplemented
|
|
575
|
+
if backend_result is not NotImplemented and backend_result is not None:
|
|
576
|
+
return backend_result
|
|
577
|
+
parsed_template = None
|
|
578
|
+
|
|
475
579
|
if self._groups_hint is not None:
|
|
476
580
|
try:
|
|
477
581
|
parsed_template = _parser.parse_template(
|