PyPcre 0.3.1__tar.gz → 0.4.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 (59) hide show
  1. {pypcre-0.3.1 → pypcre-0.4.0}/PKG-INFO +36 -17
  2. {pypcre-0.3.1 → pypcre-0.4.0}/PyPcre.egg-info/PKG-INFO +36 -17
  3. {pypcre-0.3.1 → pypcre-0.4.0}/PyPcre.egg-info/SOURCES.txt +5 -0
  4. {pypcre-0.3.1 → pypcre-0.4.0}/README.md +34 -15
  5. {pypcre-0.3.1 → pypcre-0.4.0}/pcre/pcre.py +8 -0
  6. {pypcre-0.3.1 → pypcre-0.4.0}/pcre_ext/memory.c +34 -9
  7. {pypcre-0.3.1 → pypcre-0.4.0}/pcre_ext/pcre2.c +250 -64
  8. {pypcre-0.3.1 → pypcre-0.4.0}/pcre_ext/util.c +120 -15
  9. {pypcre-0.3.1 → pypcre-0.4.0}/pyproject.toml +2 -2
  10. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_cache.py +62 -0
  11. pypcre-0.4.0/tests/test_cache_global.py +136 -0
  12. pypcre-0.4.0/tests/test_cache_global_inproc.py +163 -0
  13. pypcre-0.4.0/tests/test_coverage_gaps.py +366 -0
  14. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_errors.py +6 -0
  15. pypcre-0.4.0/tests/test_re_compat.py +221 -0
  16. pypcre-0.4.0/tests/test_threads.py +132 -0
  17. {pypcre-0.3.1 → pypcre-0.4.0}/LICENSE +0 -0
  18. {pypcre-0.3.1 → pypcre-0.4.0}/MANIFEST.in +0 -0
  19. {pypcre-0.3.1 → pypcre-0.4.0}/PyPcre.egg-info/dependency_links.txt +0 -0
  20. {pypcre-0.3.1 → pypcre-0.4.0}/PyPcre.egg-info/top_level.txt +0 -0
  21. {pypcre-0.3.1 → pypcre-0.4.0}/pcre/__init__.py +0 -0
  22. {pypcre-0.3.1 → pypcre-0.4.0}/pcre/_stdlib_re.py +0 -0
  23. {pypcre-0.3.1 → pypcre-0.4.0}/pcre/cache.py +0 -0
  24. {pypcre-0.3.1 → pypcre-0.4.0}/pcre/error.py +0 -0
  25. {pypcre-0.3.1 → pypcre-0.4.0}/pcre/flags.py +0 -0
  26. {pypcre-0.3.1 → pypcre-0.4.0}/pcre/re_compat.py +0 -0
  27. {pypcre-0.3.1 → pypcre-0.4.0}/pcre/threads.py +0 -0
  28. {pypcre-0.3.1 → pypcre-0.4.0}/pcre_ext/atomic_compat.h +0 -0
  29. {pypcre-0.3.1 → pypcre-0.4.0}/pcre_ext/cache.c +0 -0
  30. {pypcre-0.3.1 → pypcre-0.4.0}/pcre_ext/error.c +0 -0
  31. {pypcre-0.3.1 → pypcre-0.4.0}/pcre_ext/flag.c +0 -0
  32. {pypcre-0.3.1 → pypcre-0.4.0}/pcre_ext/jit_support.c +0 -0
  33. {pypcre-0.3.1 → pypcre-0.4.0}/pcre_ext/pattern_cache.c +0 -0
  34. {pypcre-0.3.1 → pypcre-0.4.0}/pcre_ext/pcre2.h +0 -0
  35. {pypcre-0.3.1 → pypcre-0.4.0}/pcre_ext/pcre2_module.h +0 -0
  36. {pypcre-0.3.1 → pypcre-0.4.0}/pcre_ext/string_helpers.c +0 -0
  37. {pypcre-0.3.1 → pypcre-0.4.0}/setup.cfg +0 -0
  38. {pypcre-0.3.1 → pypcre-0.4.0}/setup.py +0 -0
  39. {pypcre-0.3.1 → pypcre-0.4.0}/setup_utils.py +0 -0
  40. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_accuracy.py +0 -0
  41. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_api_parity.py +0 -0
  42. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_basic.py +0 -0
  43. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_bench_string.py +0 -0
  44. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_benchmark.py +0 -0
  45. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_bytes.py +0 -0
  46. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_clobber.py +0 -0
  47. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_clobber_thread.py +0 -0
  48. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_core.py +0 -0
  49. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_flags.py +0 -0
  50. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_jit.py +0 -0
  51. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_memory.py +0 -0
  52. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_module.py +0 -0
  53. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_pattern.py +0 -0
  54. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_setup_utils_multiarch.py +0 -0
  55. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_simd.py +0 -0
  56. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_threaded_backend.py +0 -0
  57. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_transformers_regex.py +0 -0
  58. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_utf8.py +0 -0
  59. {pypcre-0.3.1 → pypcre-0.4.0}/tests/test_version.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPcre
3
- Version: 0.3.1
3
+ Version: 0.4.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
- License: Apache-2.0
6
+ License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/ModelCloud/pcre
8
8
  Keywords: regex,pcre2,bindings
9
9
  Classifier: Programming Language :: Python :: 3
@@ -47,6 +47,7 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
47
47
 
48
48
 
49
49
  ## Latest News 🚀
50
+ * 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. 🛡️⚡
50
51
  * 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. ⚡
51
52
  * 03/22/2026 [0.2.15](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.15): Python 3.15 `re` compatibility (`prefixmatch`, `NOFLAG`) ✅
52
53
  * 03/21/2026 [0.2.14](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.14): Python 3.14 compatibility 🐍
@@ -85,26 +86,44 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
85
86
 
86
87
  ### Benchmark Highlights 🏁
87
88
 
88
- Measured on a `Python 3.14.3` free-threaded build on x86_64 Linux with compiled-pattern reuse. Times are best-of-5; lower is better.
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.
89
90
 
90
- | Workload | Operation | PyPcre | `re` | `regex` | PyPcre edge |
91
- | --- | --- | ---: | ---: | ---: | --- |
92
- | First `ERROR` line in a multiline log buffer | `search` | `3.68 ms` | `51.72 ms` | `5.67 ms` | `14.0x` vs `re`, `1.54x` vs `regex` |
93
- | Extract only `WARN` / `ERROR` lines | `findall` | `6.41 ms` | `91.84 ms` | `91.14 ms` | `14.3x` vs `re`, `14.2x` vs `regex` |
94
- | Per-line full-name extraction | `findall` | `22.28 ms` | `172.38 ms` | `218.29 ms` | `7.74x` vs `re`, `9.80x` vs `regex` |
95
- | Lookbehind + negative-lookahead extraction | `findall` | `50.23 ms` | `53.35 ms` | `57.03 ms` | `1.06x` vs `re`, `1.14x` vs `regex` |
96
- | UUID extraction | `findall` | `77.49 ms` | `83.19 ms` | `134.87 ms` | `1.07x` vs `re`, `1.74x` vs `regex` |
97
- | Boundary-aware token scan | `findall` | `127.76 ms` | `128.03 ms` | `146.37 ms` | effectively tied with `re`, `1.15x` vs `regex` |
91
+ A reproducible version of this benchmark lives in [`benchmarks/competitor_bench.py`](benchmarks/competitor_bench.py).
92
+
93
+ #### `findall` — large multiline and lookaround workloads
94
+
95
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
96
+ | --- | ---: | ---: | ---: | --- |
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` |
100
+
101
+ Patterns used:
102
+
103
+ ```python
104
+ # WARN/ERROR lines and full-name extraction
105
+ ^(?:WARN|ERROR).*?$ # with re.MULTILINE / pcre.Flag.MULTILINE
106
+ ^[A-Z][a-z]+ [A-Z][a-z]+ # with re.MULTILINE / pcre.Flag.MULTILINE
107
+
108
+ # lookbehind + negative lookahead
109
+ (?:(?<=foo)bar|baz)(?!qux)
110
+ ```
111
+
112
+ #### `finditer` — same workloads
113
+
114
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
115
+ | --- | ---: | ---: | ---: | --- |
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` |
98
118
 
99
119
  ### Free-Threaded Benchmark Highlights 🧵
100
120
 
101
- Measured in the same environment with `8` threads sharing one compiled pattern. Times are best-of-3; lower is better.
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.
102
122
 
103
- | Workload | Threads | PyPcre | `re` | `regex` | PyPcre edge |
104
- | --- | ---: | ---: | ---: | ---: | --- |
105
- | First `ERROR` line in a multiline log buffer | `8` | `25.34 ms` | `38.83 ms` | `40.34 ms` | `1.53x` vs `re`, `1.59x` vs `regex` |
106
- | Extract only `WARN` / `ERROR` lines | `8` | `28.58 ms` | `65.54 ms` | `73.55 ms` | `2.29x` vs `re`, `2.57x` vs `regex` |
107
- | Per-line full-name extraction | `8` | `31.68 ms` | `123.44 ms` | `164.80 ms` | `3.90x` vs `re`, `5.20x` vs `regex` |
123
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
124
+ | --- | ---: | ---: | ---: | --- |
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` |
108
127
 
109
128
  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. 🚀
110
129
 
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPcre
3
- Version: 0.3.1
3
+ Version: 0.4.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
- License: Apache-2.0
6
+ License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/ModelCloud/pcre
8
8
  Keywords: regex,pcre2,bindings
9
9
  Classifier: Programming Language :: Python :: 3
@@ -47,6 +47,7 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
47
47
 
48
48
 
49
49
  ## Latest News 🚀
50
+ * 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. 🛡️⚡
50
51
  * 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. ⚡
51
52
  * 03/22/2026 [0.2.15](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.15): Python 3.15 `re` compatibility (`prefixmatch`, `NOFLAG`) ✅
52
53
  * 03/21/2026 [0.2.14](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.14): Python 3.14 compatibility 🐍
@@ -85,26 +86,44 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
85
86
 
86
87
  ### Benchmark Highlights 🏁
87
88
 
88
- Measured on a `Python 3.14.3` free-threaded build on x86_64 Linux with compiled-pattern reuse. Times are best-of-5; lower is better.
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.
89
90
 
90
- | Workload | Operation | PyPcre | `re` | `regex` | PyPcre edge |
91
- | --- | --- | ---: | ---: | ---: | --- |
92
- | First `ERROR` line in a multiline log buffer | `search` | `3.68 ms` | `51.72 ms` | `5.67 ms` | `14.0x` vs `re`, `1.54x` vs `regex` |
93
- | Extract only `WARN` / `ERROR` lines | `findall` | `6.41 ms` | `91.84 ms` | `91.14 ms` | `14.3x` vs `re`, `14.2x` vs `regex` |
94
- | Per-line full-name extraction | `findall` | `22.28 ms` | `172.38 ms` | `218.29 ms` | `7.74x` vs `re`, `9.80x` vs `regex` |
95
- | Lookbehind + negative-lookahead extraction | `findall` | `50.23 ms` | `53.35 ms` | `57.03 ms` | `1.06x` vs `re`, `1.14x` vs `regex` |
96
- | UUID extraction | `findall` | `77.49 ms` | `83.19 ms` | `134.87 ms` | `1.07x` vs `re`, `1.74x` vs `regex` |
97
- | Boundary-aware token scan | `findall` | `127.76 ms` | `128.03 ms` | `146.37 ms` | effectively tied with `re`, `1.15x` vs `regex` |
91
+ A reproducible version of this benchmark lives in [`benchmarks/competitor_bench.py`](benchmarks/competitor_bench.py).
92
+
93
+ #### `findall` — large multiline and lookaround workloads
94
+
95
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
96
+ | --- | ---: | ---: | ---: | --- |
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` |
100
+
101
+ Patterns used:
102
+
103
+ ```python
104
+ # WARN/ERROR lines and full-name extraction
105
+ ^(?:WARN|ERROR).*?$ # with re.MULTILINE / pcre.Flag.MULTILINE
106
+ ^[A-Z][a-z]+ [A-Z][a-z]+ # with re.MULTILINE / pcre.Flag.MULTILINE
107
+
108
+ # lookbehind + negative lookahead
109
+ (?:(?<=foo)bar|baz)(?!qux)
110
+ ```
111
+
112
+ #### `finditer` — same workloads
113
+
114
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
115
+ | --- | ---: | ---: | ---: | --- |
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` |
98
118
 
99
119
  ### Free-Threaded Benchmark Highlights 🧵
100
120
 
101
- Measured in the same environment with `8` threads sharing one compiled pattern. Times are best-of-3; lower is better.
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.
102
122
 
103
- | Workload | Threads | PyPcre | `re` | `regex` | PyPcre edge |
104
- | --- | ---: | ---: | ---: | ---: | --- |
105
- | First `ERROR` line in a multiline log buffer | `8` | `25.34 ms` | `38.83 ms` | `40.34 ms` | `1.53x` vs `re`, `1.59x` vs `regex` |
106
- | Extract only `WARN` / `ERROR` lines | `8` | `28.58 ms` | `65.54 ms` | `73.55 ms` | `2.29x` vs `re`, `2.57x` vs `regex` |
107
- | Per-line full-name extraction | `8` | `31.68 ms` | `123.44 ms` | `164.80 ms` | `3.90x` vs `re`, `5.20x` vs `regex` |
123
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
124
+ | --- | ---: | ---: | ---: | --- |
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` |
108
127
 
109
128
  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. 🚀
110
129
 
@@ -35,18 +35,23 @@ tests/test_bench_string.py
35
35
  tests/test_benchmark.py
36
36
  tests/test_bytes.py
37
37
  tests/test_cache.py
38
+ tests/test_cache_global.py
39
+ tests/test_cache_global_inproc.py
38
40
  tests/test_clobber.py
39
41
  tests/test_clobber_thread.py
40
42
  tests/test_core.py
43
+ tests/test_coverage_gaps.py
41
44
  tests/test_errors.py
42
45
  tests/test_flags.py
43
46
  tests/test_jit.py
44
47
  tests/test_memory.py
45
48
  tests/test_module.py
46
49
  tests/test_pattern.py
50
+ tests/test_re_compat.py
47
51
  tests/test_setup_utils_multiarch.py
48
52
  tests/test_simd.py
49
53
  tests/test_threaded_backend.py
54
+ tests/test_threads.py
50
55
  tests/test_transformers_regex.py
51
56
  tests/test_utf8.py
52
57
  tests/test_version.py
@@ -24,6 +24,7 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
24
24
 
25
25
 
26
26
  ## Latest News 🚀
27
+ * 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. 🛡️⚡
27
28
  * 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. ⚡
28
29
  * 03/22/2026 [0.2.15](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.15): Python 3.15 `re` compatibility (`prefixmatch`, `NOFLAG`) ✅
29
30
  * 03/21/2026 [0.2.14](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.14): Python 3.14 compatibility 🐍
@@ -62,26 +63,44 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
62
63
 
63
64
  ### Benchmark Highlights 🏁
64
65
 
65
- Measured on a `Python 3.14.3` free-threaded build on x86_64 Linux with compiled-pattern reuse. Times are best-of-5; lower is better.
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.
66
67
 
67
- | Workload | Operation | PyPcre | `re` | `regex` | PyPcre edge |
68
- | --- | --- | ---: | ---: | ---: | --- |
69
- | First `ERROR` line in a multiline log buffer | `search` | `3.68 ms` | `51.72 ms` | `5.67 ms` | `14.0x` vs `re`, `1.54x` vs `regex` |
70
- | Extract only `WARN` / `ERROR` lines | `findall` | `6.41 ms` | `91.84 ms` | `91.14 ms` | `14.3x` vs `re`, `14.2x` vs `regex` |
71
- | Per-line full-name extraction | `findall` | `22.28 ms` | `172.38 ms` | `218.29 ms` | `7.74x` vs `re`, `9.80x` vs `regex` |
72
- | Lookbehind + negative-lookahead extraction | `findall` | `50.23 ms` | `53.35 ms` | `57.03 ms` | `1.06x` vs `re`, `1.14x` vs `regex` |
73
- | UUID extraction | `findall` | `77.49 ms` | `83.19 ms` | `134.87 ms` | `1.07x` vs `re`, `1.74x` vs `regex` |
74
- | Boundary-aware token scan | `findall` | `127.76 ms` | `128.03 ms` | `146.37 ms` | effectively tied with `re`, `1.15x` vs `regex` |
68
+ A reproducible version of this benchmark lives in [`benchmarks/competitor_bench.py`](benchmarks/competitor_bench.py).
69
+
70
+ #### `findall` — large multiline and lookaround workloads
71
+
72
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
73
+ | --- | ---: | ---: | ---: | --- |
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` |
77
+
78
+ Patterns used:
79
+
80
+ ```python
81
+ # WARN/ERROR lines and full-name extraction
82
+ ^(?:WARN|ERROR).*?$ # with re.MULTILINE / pcre.Flag.MULTILINE
83
+ ^[A-Z][a-z]+ [A-Z][a-z]+ # with re.MULTILINE / pcre.Flag.MULTILINE
84
+
85
+ # lookbehind + negative lookahead
86
+ (?:(?<=foo)bar|baz)(?!qux)
87
+ ```
88
+
89
+ #### `finditer` — same workloads
90
+
91
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
92
+ | --- | ---: | ---: | ---: | --- |
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` |
75
95
 
76
96
  ### Free-Threaded Benchmark Highlights 🧵
77
97
 
78
- Measured in the same environment with `8` threads sharing one compiled pattern. Times are best-of-3; lower is better.
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.
79
99
 
80
- | Workload | Threads | PyPcre | `re` | `regex` | PyPcre edge |
81
- | --- | ---: | ---: | ---: | ---: | --- |
82
- | First `ERROR` line in a multiline log buffer | `8` | `25.34 ms` | `38.83 ms` | `40.34 ms` | `1.53x` vs `re`, `1.59x` vs `regex` |
83
- | Extract only `WARN` / `ERROR` lines | `8` | `28.58 ms` | `65.54 ms` | `73.55 ms` | `2.29x` vs `re`, `2.57x` vs `regex` |
84
- | Per-line full-name extraction | `8` | `31.68 ms` | `123.44 ms` | `164.80 ms` | `3.90x` vs `re`, `5.20x` vs `regex` |
100
+ | Workload | PyPcre (ms) | `re` (ms) | `regex` (ms) | PyPcre edge |
101
+ | --- | ---: | ---: | ---: | --- |
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` |
85
104
 
86
105
  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. 🚀
87
106
 
@@ -386,6 +386,14 @@ class Pattern:
386
386
  ) -> List[Any]:
387
387
  if type(subject) is memoryview:
388
388
  subject = subject.tobytes()
389
+ backend_findall = getattr(self._pattern, "findall", None)
390
+ if backend_findall is not None:
391
+ compiled_end = -1 if endpos is None else resolve_endpos(subject, endpos)
392
+ try:
393
+ return backend_findall(subject, pos=pos, endpos=compiled_end, options=options)
394
+ except TypeError:
395
+ pass
396
+
389
397
  backend_iter = getattr(self._pattern, "finditer", None)
390
398
  if backend_iter is not None:
391
399
  compiled_end = -1 if endpos is None else resolve_endpos(subject, endpos)
@@ -25,7 +25,8 @@ static void *current_handle = NULL;
25
25
  static alloc_fn current_alloc = (alloc_fn)PyMem_Malloc;
26
26
  static free_fn current_free = (free_fn)PyMem_Free;
27
27
  static const char *current_name = "pymem";
28
- static int allocator_initialized = 0;
28
+ static ATOMIC_VAR(int) allocator_initialized = ATOMIC_VAR_INIT(0);
29
+ static atomic_flag allocator_init_lock = ATOMIC_FLAG_INIT;
29
30
 
30
31
  #if defined(_WIN32)
31
32
  static int
@@ -95,7 +96,19 @@ equals_ignore_case(const char *value, const char *target)
95
96
  int
96
97
  pcre_memory_initialize(void)
97
98
  {
98
- if (allocator_initialized) {
99
+ if (atomic_load_explicit(&allocator_initialized, memory_order_acquire)) {
100
+ return 0;
101
+ }
102
+
103
+ while (atomic_flag_test_and_set_explicit(&allocator_init_lock, memory_order_acq_rel)) {
104
+ if (atomic_load_explicit(&allocator_initialized, memory_order_acquire)) {
105
+ atomic_flag_clear_explicit(&allocator_init_lock, memory_order_release);
106
+ return 0;
107
+ }
108
+ }
109
+
110
+ if (atomic_load_explicit(&allocator_initialized, memory_order_acquire)) {
111
+ atomic_flag_clear_explicit(&allocator_init_lock, memory_order_release);
99
112
  return 0;
100
113
  }
101
114
 
@@ -131,7 +144,8 @@ pcre_memory_initialize(void)
131
144
  current_alloc = (alloc_fn)PyMem_Malloc;
132
145
  current_free = (free_fn)PyMem_Free;
133
146
  current_name = "pymem";
134
- allocator_initialized = 1;
147
+ atomic_store_explicit(&allocator_initialized, 1, memory_order_release);
148
+ atomic_flag_clear_explicit(&allocator_init_lock, memory_order_release);
135
149
  return 0;
136
150
  }
137
151
 
@@ -140,18 +154,21 @@ pcre_memory_initialize(void)
140
154
  current_alloc = malloc;
141
155
  current_free = free;
142
156
  current_name = "malloc";
143
- allocator_initialized = 1;
157
+ atomic_store_explicit(&allocator_initialized, 1, memory_order_release);
158
+ atomic_flag_clear_explicit(&allocator_init_lock, memory_order_release);
144
159
  return 0;
145
160
  }
146
161
 
147
162
  if (equals_ignore_case(forced, "jemalloc")) {
148
163
  if (load_allocator(&jemalloc_candidate) == 0) {
149
- allocator_initialized = 1;
164
+ atomic_store_explicit(&allocator_initialized, 1, memory_order_release);
165
+ atomic_flag_clear_explicit(&allocator_init_lock, memory_order_release);
150
166
  return 0;
151
167
  }
152
168
  } else if (equals_ignore_case(forced, "tcmalloc")) {
153
169
  if (load_allocator(&tcmalloc_candidate) == 0) {
154
- allocator_initialized = 1;
170
+ atomic_store_explicit(&allocator_initialized, 1, memory_order_release);
171
+ atomic_flag_clear_explicit(&allocator_init_lock, memory_order_release);
155
172
  return 0;
156
173
  }
157
174
  }
@@ -160,7 +177,8 @@ pcre_memory_initialize(void)
160
177
  current_alloc = (alloc_fn)PyMem_Malloc;
161
178
  current_free = (free_fn)PyMem_Free;
162
179
  current_name = "pymem";
163
- allocator_initialized = 1;
180
+ atomic_store_explicit(&allocator_initialized, 1, memory_order_release);
181
+ atomic_flag_clear_explicit(&allocator_init_lock, memory_order_release);
164
182
  return 0;
165
183
  }
166
184
 
@@ -177,13 +195,14 @@ pcre_memory_teardown(void)
177
195
  current_alloc = (alloc_fn)PyMem_Malloc;
178
196
  current_free = (free_fn)PyMem_Free;
179
197
  current_name = "pymem";
180
- allocator_initialized = 0;
198
+ atomic_store_explicit(&allocator_initialized, 0, memory_order_release);
199
+ atomic_flag_clear_explicit(&allocator_init_lock, memory_order_release);
181
200
  }
182
201
 
183
202
  void *
184
203
  pcre_malloc(size_t size)
185
204
  {
186
- if (!allocator_initialized) {
205
+ if (!atomic_load_explicit(&allocator_initialized, memory_order_acquire)) {
187
206
  if (pcre_memory_initialize() != 0) {
188
207
  return NULL;
189
208
  }
@@ -197,11 +216,17 @@ pcre_free(void *ptr)
197
216
  if (ptr == NULL) {
198
217
  return;
199
218
  }
219
+ if (!atomic_load_explicit(&allocator_initialized, memory_order_acquire)) {
220
+ (void)pcre_memory_initialize();
221
+ }
200
222
  current_free(ptr);
201
223
  }
202
224
 
203
225
  const char *
204
226
  pcre_memory_allocator_name(void)
205
227
  {
228
+ if (!atomic_load_explicit(&allocator_initialized, memory_order_acquire)) {
229
+ (void)pcre_memory_initialize();
230
+ }
206
231
  return current_name;
207
232
  }