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.
Files changed (65) hide show
  1. {pypcre-0.4.0 → pypcre-0.6.0}/PKG-INFO +44 -10
  2. {pypcre-0.4.0 → pypcre-0.6.0}/PyPcre.egg-info/PKG-INFO +44 -10
  3. {pypcre-0.4.0 → pypcre-0.6.0}/PyPcre.egg-info/SOURCES.txt +6 -0
  4. {pypcre-0.4.0 → pypcre-0.6.0}/README.md +43 -9
  5. {pypcre-0.4.0 → pypcre-0.6.0}/pcre/__init__.py +2 -1
  6. {pypcre-0.4.0 → pypcre-0.6.0}/pcre/pcre.py +158 -54
  7. {pypcre-0.4.0 → pypcre-0.6.0}/pcre/re_compat.py +17 -3
  8. {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/cache.c +165 -72
  9. {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/flag.c +8 -4
  10. {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/pattern_cache.c +95 -5
  11. {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/pcre2.c +2062 -306
  12. {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/pcre2_module.h +14 -1
  13. {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/string_helpers.c +66 -34
  14. {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/util.c +94 -3
  15. {pypcre-0.4.0 → pypcre-0.6.0}/pyproject.toml +1 -1
  16. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_api_parity.py +91 -0
  17. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_bytes.py +6 -8
  18. pypcre-0.6.0/tests/test_c_api_audit.py +349 -0
  19. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_cache.py +4 -1
  20. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_coverage_gaps.py +41 -0
  21. pypcre-0.6.0/tests/test_gil_zero_safety.py +128 -0
  22. pypcre-0.6.0/tests/test_jit_anchoring.py +166 -0
  23. pypcre-0.6.0/tests/test_lastindex_semantics.py +115 -0
  24. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_memory.py +66 -8
  25. pypcre-0.6.0/tests/test_mmap.py +139 -0
  26. pypcre-0.6.0/tests/test_python_coverage_audit.py +348 -0
  27. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_re_compat.py +2 -3
  28. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_threads.py +4 -4
  29. {pypcre-0.4.0 → pypcre-0.6.0}/LICENSE +0 -0
  30. {pypcre-0.4.0 → pypcre-0.6.0}/MANIFEST.in +0 -0
  31. {pypcre-0.4.0 → pypcre-0.6.0}/PyPcre.egg-info/dependency_links.txt +0 -0
  32. {pypcre-0.4.0 → pypcre-0.6.0}/PyPcre.egg-info/top_level.txt +0 -0
  33. {pypcre-0.4.0 → pypcre-0.6.0}/pcre/_stdlib_re.py +0 -0
  34. {pypcre-0.4.0 → pypcre-0.6.0}/pcre/cache.py +0 -0
  35. {pypcre-0.4.0 → pypcre-0.6.0}/pcre/error.py +0 -0
  36. {pypcre-0.4.0 → pypcre-0.6.0}/pcre/flags.py +0 -0
  37. {pypcre-0.4.0 → pypcre-0.6.0}/pcre/threads.py +0 -0
  38. {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/atomic_compat.h +0 -0
  39. {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/error.c +0 -0
  40. {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/jit_support.c +0 -0
  41. {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/memory.c +0 -0
  42. {pypcre-0.4.0 → pypcre-0.6.0}/pcre_ext/pcre2.h +0 -0
  43. {pypcre-0.4.0 → pypcre-0.6.0}/setup.cfg +0 -0
  44. {pypcre-0.4.0 → pypcre-0.6.0}/setup.py +0 -0
  45. {pypcre-0.4.0 → pypcre-0.6.0}/setup_utils.py +0 -0
  46. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_accuracy.py +0 -0
  47. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_basic.py +0 -0
  48. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_bench_string.py +0 -0
  49. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_benchmark.py +0 -0
  50. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_cache_global.py +0 -0
  51. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_cache_global_inproc.py +0 -0
  52. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_clobber.py +0 -0
  53. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_clobber_thread.py +0 -0
  54. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_core.py +0 -0
  55. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_errors.py +0 -0
  56. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_flags.py +0 -0
  57. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_jit.py +0 -0
  58. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_module.py +0 -0
  59. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_pattern.py +0 -0
  60. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_setup_utils_multiarch.py +0 -0
  61. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_simd.py +0 -0
  62. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_threaded_backend.py +0 -0
  63. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_transformers_regex.py +0 -0
  64. {pypcre-0.4.0 → pypcre-0.6.0}/tests/test_utf8.py +0 -0
  65. {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.4.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.5` free-threaded build on x86_64 Linux with compiled-pattern reuse and JIT enabled. Times are the best of several runs; lower is better. Only workloads where PyPcre is decisively faster than both `stdlib.re` and `regex` are shown.
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.60` | `29.35` | `30.85` | **49x** vs `re`, **52x** vs `regex` |
98
- | Per-line full-name extraction (multiline) | `1.02` | `29.50` | `15.95` | **29x** vs `re`, **16x** vs `regex` |
99
- | Lookbehind + negative-lookahead tokens | `3.78` | `11.81` | `10.42` | **3.1x** vs `re`, **2.8x** vs `regex` |
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
- | Extract `WARN` / `ERROR` lines | `0.60` | `29.33` | `31.23` | **49x** vs `re`, **52x** vs `regex` |
117
- | Per-line full-name extraction | `1.02` | `30.09` | `16.25` | **29x** vs `re`, **16x** vs `regex` |
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 in the same environment with `8` threads fanning out over independent copies of each workload. Times are the best of several runs; lower is better.
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`) | `3.26` | `32.57` | `34.41` | **10.0x** vs `re`, **10.6x** vs `regex` |
126
- | Per-line full-name extraction (`findall`) | `3.18` | `31.84` | `17.31` | **10.0x** vs `re`, **5.4x** vs `regex` |
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.4.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.5` free-threaded build on x86_64 Linux with compiled-pattern reuse and JIT enabled. Times are the best of several runs; lower is better. Only workloads where PyPcre is decisively faster than both `stdlib.re` and `regex` are shown.
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.60` | `29.35` | `30.85` | **49x** vs `re`, **52x** vs `regex` |
98
- | Per-line full-name extraction (multiline) | `1.02` | `29.50` | `15.95` | **29x** vs `re`, **16x** vs `regex` |
99
- | Lookbehind + negative-lookahead tokens | `3.78` | `11.81` | `10.42` | **3.1x** vs `re`, **2.8x** vs `regex` |
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
- | Extract `WARN` / `ERROR` lines | `0.60` | `29.33` | `31.23` | **49x** vs `re`, **52x** vs `regex` |
117
- | Per-line full-name extraction | `1.02` | `30.09` | `16.25` | **29x** vs `re`, **16x** vs `regex` |
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 in the same environment with `8` threads fanning out over independent copies of each workload. Times are the best of several runs; lower is better.
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`) | `3.26` | `32.57` | `34.41` | **10.0x** vs `re`, **10.6x** vs `regex` |
126
- | Per-line full-name extraction (`findall`) | `3.18` | `31.84` | `17.31` | **10.0x** vs `re`, **5.4x** vs `regex` |
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.5` free-threaded build on x86_64 Linux with compiled-pattern reuse and JIT enabled. Times are the best of several runs; lower is better. Only workloads where PyPcre is decisively faster than both `stdlib.re` and `regex` are shown.
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.60` | `29.35` | `30.85` | **49x** vs `re`, **52x** vs `regex` |
75
- | Per-line full-name extraction (multiline) | `1.02` | `29.50` | `15.95` | **29x** vs `re`, **16x** vs `regex` |
76
- | Lookbehind + negative-lookahead tokens | `3.78` | `11.81` | `10.42` | **3.1x** vs `re`, **2.8x** vs `regex` |
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
- | Extract `WARN` / `ERROR` lines | `0.60` | `29.33` | `31.23` | **49x** vs `re`, **52x** vs `regex` |
94
- | Per-line full-name extraction | `1.02` | `30.09` | `16.25` | **29x** vs `re`, **16x** vs `regex` |
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 in the same environment with `8` threads fanning out over independent copies of each workload. Times are the best of several runs; lower is better.
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`) | `3.26` | `32.57` | `34.41` | **10.0x** vs `re`, **10.6x** vs `regex` |
103
- | Per-line full-name extraction (`findall`) | `3.18` | `31.84` | `17.31` | **10.0x** vs `re`, **5.4x** vs `regex` |
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 Generator, Iterable
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) -> dict[str, int]:
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
- if raw is None:
274
- return None
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
- if raw is None:
302
- return None
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
- if raw is None:
328
- return None
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
- ) -> Generator[Match, None, None]:
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
- raw_iter = None
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
- for raw in raw_iter:
354
- yield self._wrap_match(raw, subject, origin_pos, resolved_end)
355
- return
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
- while True:
362
- raw = self._pattern.search(subject, pos=current, endpos=search_end, options=options)
363
- if raw is None:
364
- break
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
- match_obj = self._wrap_match(raw, subject, origin_pos, resolved_end)
367
- yield match_obj
429
+ match_obj = self._wrap_match(raw, subject, origin_pos, resolved_end)
430
+ yield match_obj
368
431
 
369
- start, end = match_obj.span()
370
- next_pos = compute_next_pos(current, (start, end), endpos)
371
- if next_pos <= current:
372
- next_pos = current + 1
373
- current = next_pos
374
- if current > subject_length:
375
- break
376
- if endpos is not None and current >= resolved_end:
377
- break
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(