PyPcre 0.6.2__tar.gz → 0.6.3__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 (80) hide show
  1. {pypcre-0.6.2 → pypcre-0.6.3}/PKG-INFO +17 -13
  2. {pypcre-0.6.2 → pypcre-0.6.3}/PyPcre.egg-info/PKG-INFO +17 -13
  3. {pypcre-0.6.2 → pypcre-0.6.3}/README.md +16 -12
  4. {pypcre-0.6.2 → pypcre-0.6.3}/pcre/pcre.py +244 -24
  5. {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/pcre2.c +36 -14
  6. {pypcre-0.6.2 → pypcre-0.6.3}/pyproject.toml +1 -1
  7. {pypcre-0.6.2 → pypcre-0.6.3}/setup.py +1 -1
  8. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_lastindex_semantics.py +20 -0
  9. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_memory.py +47 -0
  10. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_version.py +15 -0
  11. {pypcre-0.6.2 → pypcre-0.6.3}/LICENSE +0 -0
  12. {pypcre-0.6.2 → pypcre-0.6.3}/MANIFEST.in +0 -0
  13. {pypcre-0.6.2 → pypcre-0.6.3}/PyPcre.egg-info/SOURCES.txt +0 -0
  14. {pypcre-0.6.2 → pypcre-0.6.3}/PyPcre.egg-info/dependency_links.txt +0 -0
  15. {pypcre-0.6.2 → pypcre-0.6.3}/PyPcre.egg-info/top_level.txt +0 -0
  16. {pypcre-0.6.2 → pypcre-0.6.3}/pcre/__init__.py +0 -0
  17. {pypcre-0.6.2 → pypcre-0.6.3}/pcre/_stdlib_re.py +0 -0
  18. {pypcre-0.6.2 → pypcre-0.6.3}/pcre/cache.py +0 -0
  19. {pypcre-0.6.2 → pypcre-0.6.3}/pcre/error.py +0 -0
  20. {pypcre-0.6.2 → pypcre-0.6.3}/pcre/flags.py +0 -0
  21. {pypcre-0.6.2 → pypcre-0.6.3}/pcre/re_compat.py +0 -0
  22. {pypcre-0.6.2 → pypcre-0.6.3}/pcre/threads.py +0 -0
  23. {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/atomic_compat.h +0 -0
  24. {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/cache.c +0 -0
  25. {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/error.c +0 -0
  26. {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/flag.c +0 -0
  27. {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/jit_support.c +0 -0
  28. {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/memory.c +0 -0
  29. {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/pattern_cache.c +0 -0
  30. {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/pcre2.h +0 -0
  31. {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/pcre2_module.h +0 -0
  32. {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/string_helpers.c +0 -0
  33. {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/util.c +0 -0
  34. {pypcre-0.6.2 → pypcre-0.6.3}/setup.cfg +0 -0
  35. {pypcre-0.6.2 → pypcre-0.6.3}/setup_utils.py +0 -0
  36. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_accuracy.py +0 -0
  37. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_api_parity.py +0 -0
  38. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_basic.py +0 -0
  39. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_bench_string.py +0 -0
  40. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_benchmark.py +0 -0
  41. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_bytes.py +0 -0
  42. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_c_api_audit.py +0 -0
  43. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_cache.py +0 -0
  44. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_cache_global.py +0 -0
  45. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_cache_global_inproc.py +0 -0
  46. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_cache_scope_safety.py +0 -0
  47. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_clobber.py +0 -0
  48. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_clobber_thread.py +0 -0
  49. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_clobber_verify.py +0 -0
  50. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_compile_utf_no_check_safety.py +0 -0
  51. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_core.py +0 -0
  52. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_coverage_gaps.py +0 -0
  53. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_errors.py +0 -0
  54. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_escape_fastpath.py +0 -0
  55. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_expand_fastpath.py +0 -0
  56. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_flags.py +0 -0
  57. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_gil_zero_safety.py +0 -0
  58. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_jit.py +0 -0
  59. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_jit_anchoring.py +0 -0
  60. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_jit_start_optimize_workaround.py +0 -0
  61. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_lastindex_ovector_fastpath.py +0 -0
  62. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_literal_capture_findall_fastpath.py +0 -0
  63. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_literal_capture_split_fastpath.py +0 -0
  64. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_mmap.py +0 -0
  65. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_module.py +0 -0
  66. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_multi_literal_capture_findall_fastpath.py +0 -0
  67. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_multi_literal_capture_split_fastpath.py +0 -0
  68. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_pattern.py +0 -0
  69. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_python_coverage_audit.py +0 -0
  70. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_re_compat.py +0 -0
  71. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_setup_utils_multiarch.py +0 -0
  72. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_simd.py +0 -0
  73. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_stdlib_flag_fastpath.py +0 -0
  74. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_sub_count_one_fastpath.py +0 -0
  75. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_sub_reference_fastpath.py +0 -0
  76. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_template_dispatch_fastpath.py +0 -0
  77. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_threaded_backend.py +0 -0
  78. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_threads.py +0 -0
  79. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_transformers_regex.py +0 -0
  80. {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_utf8.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPcre
3
- Version: 0.6.2
3
+ Version: 0.6.3
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
@@ -37,7 +37,7 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
37
37
  </p>
38
38
 
39
39
  <p align="center">
40
- <a href="https://github.com/ModelCloud/PyPcre/releases" style="text-decoration:none;"><img alt="GitHub release" src="https://img.shields.io/github/release/ModelCloud/Pcre.svg"></a>
40
+ <a href="https://github.com/ModelCloud/PyPcre/releases" style="text-decoration:none;"><img alt="GitHub release" src="https://img.shields.io/github/release/ModelCloud/PyPcre.svg"></a>
41
41
  <a href="https://pypi.org/project/PyPcre/" style="text-decoration:none;"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/PyPcre"></a>
42
42
  <a href="https://pepy.tech/projects/PyPcre" style="text-decoration:none;"><img src="https://static.pepy.tech/badge/PyPcre" alt="PyPI Downloads"></a>
43
43
  <a href="https://github.com/ModelCloud/PyPcre/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/PyPcre"></a>
@@ -47,8 +47,10 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
47
47
 
48
48
 
49
49
  ## Latest News 🚀
50
- * 08/09–08/10/2026 **API performance and safety update**: common regex workloads and ordered `parallel_map(search)` workloads were benchmarked across Python 3.10 and free-threaded Python 3.14t/GIL=0. Compatibility fallbacks preserve complex patterns, subclasses, buffers, and callables. Unsafe UTF bytes compilation is also blocked, with differential, randomized, concurrency, subprocess, and memory-safety coverage. 🧵⚡🛡️
51
- * 08/08/2026 **0.6.0**: `findall`, `finditer`, `sub`/`subn`, `split`, and `match`/`search`/`fullmatch` are now up to **46x faster** than `stdlib.re` and **48x faster** than `regex` on `finditer`/`findall` workloads, **13x** on `split`, and **2–9x** on `sub`/`subn` backref workloads, with full `re` semantics. Free-threaded `findall` reaches **13.8x** vs `re` on 8 threads. 🚀⚡
50
+ * 10/20/2026 **[0.6.3](https://github.com/ModelCloud/PyPcre/releases/tag/v0.6.3)**: Fixed free-threaded races in `Match.lastindex` replay and PCRE2 version initialization, and reduced Python dispatch overhead across module-level and compiled-pattern `match`, `search`, `fullmatch`, `findall`, `finditer`, `split`, `sub`, and `subn` hot paths, with new concurrency and GC-cycle regression coverage plus exact fallbacks for subclasses, legacy backends, and non-default arguments. 🛡️🧵🚀⚡
51
+ * 08/25/2026 **[0.6.2](https://github.com/ModelCloud/PyPcre/releases/tag/v0.6.2)**: Hardened packaging metadata and fixed FreeBSD/WSL CI failures on PCRE2 10.42 runtimes, including legacy replacement-template and JIT-fallback compatibility. 🛠️🐧🪟
52
+ * 08/25/2026 **[0.6.1](https://github.com/ModelCloud/PyPcre/releases/tag/v0.6.1)**: Workaround for PCRE2 10.46/10.47 JIT start-optimization regressions, new verifying clobber suite, second memory/thread-safety sweep, Python 3.15 `sre_parse` removal compatibility, aligned `Pattern` `pos`/`endpos` signatures, and `setuptools` metadata fixes. 🛡️🧵🐍
53
+ * 08/09/2026 **[0.6.0](https://github.com/ModelCloud/PyPcre/releases/tag/v0.6.0)**: `findall`, `finditer`, `sub`/`subn`, `split`, and `match`/`search`/`fullmatch` hot-path speedups up to **46x** vs `stdlib.re` and **48x** vs `regex`, with free-threaded `findall` reaching **13.8x** on 8 threads. 🚀⚡
52
54
  * 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. 🗂️⚡
53
55
  * 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. 🛡️⚡
54
56
  * 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. ⚡
@@ -83,7 +85,7 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
83
85
  | Syntax power | Very rich ✅ | More limited | Rich, but different from `PCRE2` |
84
86
  | JIT execution | `PCRE2` JIT ✅ | No | No |
85
87
  | `re`-compatible API surface | Stable and familiar ✅ | Native | Similar, but not the main goal |
86
- | Free-threaded support | Built and tested for `PYTHON_GIL=0` ✅ | No explicit PyPcre-style layer | Not a project focus here |
88
+ | Free-threaded support | Built and tested for `PYTHON_GIL=0` ✅ | No explicit free-threaded support | No explicit free-threaded fan-out layer |
87
89
  | Built-in threaded subject fan-out | `parallel_map()` ✅ | No | No |
88
90
  | System library updates | Uses system `libpcre2-8` by default ✅ | N/A | N/A |
89
91
 
@@ -252,8 +254,10 @@ numbers = pattern.findall(b"line 1\nline 22")
252
254
  ### API Overview 🧭
253
255
 
254
256
  - Module helpers: `prefixmatch`, `match`, `search`, `fullmatch`, `finditer`,
255
- `findall`, `split`, `sub`, `subn`, `compile`, `escape`, `purge`, and
256
- `parallel_map`.
257
+ `findall`, `split`, `sub`, `subn`, `compile`, `escape`, `purge`,
258
+ `template`, `parallel_map`, `configure`, `configure_threads`,
259
+ `configure_thread_pool`, `shutdown_thread_pool`, `set_cache_limit`,
260
+ `get_cache_limit`, and `clear_cache`.
257
261
  - `compile()` returns a `Pattern` object with the familiar matching helpers
258
262
  plus `split()`, `sub()`, and `subn()`.
259
263
  - `Pattern` exposes `.pattern`, `.flags`, `.jit`, `.groupindex`, and `.groups`
@@ -269,7 +273,7 @@ numbers = pattern.findall(b"line 1\nline 22")
269
273
 
270
274
  ### Common examples 🧪
271
275
 
272
- Compiled patterns:
276
+ 🧪 Compiled patterns:
273
277
 
274
278
  ```python
275
279
  from pcre import compile, Flag
@@ -279,7 +283,7 @@ match = pattern.search("User: alice")
279
283
  print(match.group("name")) # alice
280
284
  ```
281
285
 
282
- Substitution:
286
+ 🔁 Substitution:
283
287
 
284
288
  ```python
285
289
  from pcre import sub
@@ -288,7 +292,7 @@ result = sub(r"\d+", "#", "room 101")
288
292
  print(result) # room #
289
293
  ```
290
294
 
291
- Bytes:
295
+ 🗂️ Bytes:
292
296
 
293
297
  ```python
294
298
  from pcre import compile
@@ -353,9 +357,9 @@ the conversion without repeating the flag.
353
357
 
354
358
  ### Optional runtime controls 🎛️
355
359
 
356
- - `pcre.configure(jit=False)` disables JIT globally. `Flag.JIT` and
360
+ - ⚙️ `pcre.configure(jit=False)` disables JIT globally. `Flag.JIT` and
357
361
  `Flag.NO_JIT` let you override that per pattern.
358
- - `pcre.set_cache_limit()`, `pcre.get_cache_limit()`, and `pcre.clear_cache()`
362
+ - 🧹 `pcre.set_cache_limit()`, `pcre.get_cache_limit()`, and `pcre.clear_cache()`
359
363
  control every high-level compile/template helper cache in the active context.
360
364
  A zero limit disables them, and `None` uses a 256-entry hard safety ceiling
361
365
  rather than permitting unbounded growth. High-level cache entries never cross
@@ -363,7 +367,7 @@ the conversion without repeating the flag.
363
367
  workers' high-level helper caches on their next cache-backed call. Backend
364
368
  scratch buffers remain thread-scoped and are released when that thread exits.
365
369
  Oversized patterns and templates are not retained.
366
- - `pcre.configure_threads()`, `pcre.configure_thread_pool()`,
370
+ - 🧵 `pcre.configure_threads()`, `pcre.configure_thread_pool()`,
367
371
  `shutdown_thread_pool()`, `Flag.THREADS`, and `Flag.NO_THREADS` are available
368
372
  if you want to opt into or restrict threaded execution.
369
373
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPcre
3
- Version: 0.6.2
3
+ Version: 0.6.3
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
@@ -37,7 +37,7 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
37
37
  </p>
38
38
 
39
39
  <p align="center">
40
- <a href="https://github.com/ModelCloud/PyPcre/releases" style="text-decoration:none;"><img alt="GitHub release" src="https://img.shields.io/github/release/ModelCloud/Pcre.svg"></a>
40
+ <a href="https://github.com/ModelCloud/PyPcre/releases" style="text-decoration:none;"><img alt="GitHub release" src="https://img.shields.io/github/release/ModelCloud/PyPcre.svg"></a>
41
41
  <a href="https://pypi.org/project/PyPcre/" style="text-decoration:none;"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/PyPcre"></a>
42
42
  <a href="https://pepy.tech/projects/PyPcre" style="text-decoration:none;"><img src="https://static.pepy.tech/badge/PyPcre" alt="PyPI Downloads"></a>
43
43
  <a href="https://github.com/ModelCloud/PyPcre/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/PyPcre"></a>
@@ -47,8 +47,10 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
47
47
 
48
48
 
49
49
  ## Latest News 🚀
50
- * 08/09–08/10/2026 **API performance and safety update**: common regex workloads and ordered `parallel_map(search)` workloads were benchmarked across Python 3.10 and free-threaded Python 3.14t/GIL=0. Compatibility fallbacks preserve complex patterns, subclasses, buffers, and callables. Unsafe UTF bytes compilation is also blocked, with differential, randomized, concurrency, subprocess, and memory-safety coverage. 🧵⚡🛡️
51
- * 08/08/2026 **0.6.0**: `findall`, `finditer`, `sub`/`subn`, `split`, and `match`/`search`/`fullmatch` are now up to **46x faster** than `stdlib.re` and **48x faster** than `regex` on `finditer`/`findall` workloads, **13x** on `split`, and **2–9x** on `sub`/`subn` backref workloads, with full `re` semantics. Free-threaded `findall` reaches **13.8x** vs `re` on 8 threads. 🚀⚡
50
+ * 10/20/2026 **[0.6.3](https://github.com/ModelCloud/PyPcre/releases/tag/v0.6.3)**: Fixed free-threaded races in `Match.lastindex` replay and PCRE2 version initialization, and reduced Python dispatch overhead across module-level and compiled-pattern `match`, `search`, `fullmatch`, `findall`, `finditer`, `split`, `sub`, and `subn` hot paths, with new concurrency and GC-cycle regression coverage plus exact fallbacks for subclasses, legacy backends, and non-default arguments. 🛡️🧵🚀⚡
51
+ * 08/25/2026 **[0.6.2](https://github.com/ModelCloud/PyPcre/releases/tag/v0.6.2)**: Hardened packaging metadata and fixed FreeBSD/WSL CI failures on PCRE2 10.42 runtimes, including legacy replacement-template and JIT-fallback compatibility. 🛠️🐧🪟
52
+ * 08/25/2026 **[0.6.1](https://github.com/ModelCloud/PyPcre/releases/tag/v0.6.1)**: Workaround for PCRE2 10.46/10.47 JIT start-optimization regressions, new verifying clobber suite, second memory/thread-safety sweep, Python 3.15 `sre_parse` removal compatibility, aligned `Pattern` `pos`/`endpos` signatures, and `setuptools` metadata fixes. 🛡️🧵🐍
53
+ * 08/09/2026 **[0.6.0](https://github.com/ModelCloud/PyPcre/releases/tag/v0.6.0)**: `findall`, `finditer`, `sub`/`subn`, `split`, and `match`/`search`/`fullmatch` hot-path speedups up to **46x** vs `stdlib.re` and **48x** vs `regex`, with free-threaded `findall` reaching **13.8x** on 8 threads. 🚀⚡
52
54
  * 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. 🗂️⚡
53
55
  * 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. 🛡️⚡
54
56
  * 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. ⚡
@@ -83,7 +85,7 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
83
85
  | Syntax power | Very rich ✅ | More limited | Rich, but different from `PCRE2` |
84
86
  | JIT execution | `PCRE2` JIT ✅ | No | No |
85
87
  | `re`-compatible API surface | Stable and familiar ✅ | Native | Similar, but not the main goal |
86
- | Free-threaded support | Built and tested for `PYTHON_GIL=0` ✅ | No explicit PyPcre-style layer | Not a project focus here |
88
+ | Free-threaded support | Built and tested for `PYTHON_GIL=0` ✅ | No explicit free-threaded support | No explicit free-threaded fan-out layer |
87
89
  | Built-in threaded subject fan-out | `parallel_map()` ✅ | No | No |
88
90
  | System library updates | Uses system `libpcre2-8` by default ✅ | N/A | N/A |
89
91
 
@@ -252,8 +254,10 @@ numbers = pattern.findall(b"line 1\nline 22")
252
254
  ### API Overview 🧭
253
255
 
254
256
  - Module helpers: `prefixmatch`, `match`, `search`, `fullmatch`, `finditer`,
255
- `findall`, `split`, `sub`, `subn`, `compile`, `escape`, `purge`, and
256
- `parallel_map`.
257
+ `findall`, `split`, `sub`, `subn`, `compile`, `escape`, `purge`,
258
+ `template`, `parallel_map`, `configure`, `configure_threads`,
259
+ `configure_thread_pool`, `shutdown_thread_pool`, `set_cache_limit`,
260
+ `get_cache_limit`, and `clear_cache`.
257
261
  - `compile()` returns a `Pattern` object with the familiar matching helpers
258
262
  plus `split()`, `sub()`, and `subn()`.
259
263
  - `Pattern` exposes `.pattern`, `.flags`, `.jit`, `.groupindex`, and `.groups`
@@ -269,7 +273,7 @@ numbers = pattern.findall(b"line 1\nline 22")
269
273
 
270
274
  ### Common examples 🧪
271
275
 
272
- Compiled patterns:
276
+ 🧪 Compiled patterns:
273
277
 
274
278
  ```python
275
279
  from pcre import compile, Flag
@@ -279,7 +283,7 @@ match = pattern.search("User: alice")
279
283
  print(match.group("name")) # alice
280
284
  ```
281
285
 
282
- Substitution:
286
+ 🔁 Substitution:
283
287
 
284
288
  ```python
285
289
  from pcre import sub
@@ -288,7 +292,7 @@ result = sub(r"\d+", "#", "room 101")
288
292
  print(result) # room #
289
293
  ```
290
294
 
291
- Bytes:
295
+ 🗂️ Bytes:
292
296
 
293
297
  ```python
294
298
  from pcre import compile
@@ -353,9 +357,9 @@ the conversion without repeating the flag.
353
357
 
354
358
  ### Optional runtime controls 🎛️
355
359
 
356
- - `pcre.configure(jit=False)` disables JIT globally. `Flag.JIT` and
360
+ - ⚙️ `pcre.configure(jit=False)` disables JIT globally. `Flag.JIT` and
357
361
  `Flag.NO_JIT` let you override that per pattern.
358
- - `pcre.set_cache_limit()`, `pcre.get_cache_limit()`, and `pcre.clear_cache()`
362
+ - 🧹 `pcre.set_cache_limit()`, `pcre.get_cache_limit()`, and `pcre.clear_cache()`
359
363
  control every high-level compile/template helper cache in the active context.
360
364
  A zero limit disables them, and `None` uses a 256-entry hard safety ceiling
361
365
  rather than permitting unbounded growth. High-level cache entries never cross
@@ -363,7 +367,7 @@ the conversion without repeating the flag.
363
367
  workers' high-level helper caches on their next cache-backed call. Backend
364
368
  scratch buffers remain thread-scoped and are released when that thread exits.
365
369
  Oversized patterns and templates are not retained.
366
- - `pcre.configure_threads()`, `pcre.configure_thread_pool()`,
370
+ - 🧵 `pcre.configure_threads()`, `pcre.configure_thread_pool()`,
367
371
  `shutdown_thread_pool()`, `Flag.THREADS`, and `Flag.NO_THREADS` are available
368
372
  if you want to opt into or restrict threaded execution.
369
373
 
@@ -14,7 +14,7 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
14
14
  </p>
15
15
 
16
16
  <p align="center">
17
- <a href="https://github.com/ModelCloud/PyPcre/releases" style="text-decoration:none;"><img alt="GitHub release" src="https://img.shields.io/github/release/ModelCloud/Pcre.svg"></a>
17
+ <a href="https://github.com/ModelCloud/PyPcre/releases" style="text-decoration:none;"><img alt="GitHub release" src="https://img.shields.io/github/release/ModelCloud/PyPcre.svg"></a>
18
18
  <a href="https://pypi.org/project/PyPcre/" style="text-decoration:none;"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/PyPcre"></a>
19
19
  <a href="https://pepy.tech/projects/PyPcre" style="text-decoration:none;"><img src="https://static.pepy.tech/badge/PyPcre" alt="PyPI Downloads"></a>
20
20
  <a href="https://github.com/ModelCloud/PyPcre/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/PyPcre"></a>
@@ -24,8 +24,10 @@ Fast, free-threaded Python bindings for `PCRE2` with a stable `stdlib.re`-compat
24
24
 
25
25
 
26
26
  ## Latest News 🚀
27
- * 08/09–08/10/2026 **API performance and safety update**: common regex workloads and ordered `parallel_map(search)` workloads were benchmarked across Python 3.10 and free-threaded Python 3.14t/GIL=0. Compatibility fallbacks preserve complex patterns, subclasses, buffers, and callables. Unsafe UTF bytes compilation is also blocked, with differential, randomized, concurrency, subprocess, and memory-safety coverage. 🧵⚡🛡️
28
- * 08/08/2026 **0.6.0**: `findall`, `finditer`, `sub`/`subn`, `split`, and `match`/`search`/`fullmatch` are now up to **46x faster** than `stdlib.re` and **48x faster** than `regex` on `finditer`/`findall` workloads, **13x** on `split`, and **2–9x** on `sub`/`subn` backref workloads, with full `re` semantics. Free-threaded `findall` reaches **13.8x** vs `re` on 8 threads. 🚀⚡
27
+ * 10/20/2026 **[0.6.3](https://github.com/ModelCloud/PyPcre/releases/tag/v0.6.3)**: Fixed free-threaded races in `Match.lastindex` replay and PCRE2 version initialization, and reduced Python dispatch overhead across module-level and compiled-pattern `match`, `search`, `fullmatch`, `findall`, `finditer`, `split`, `sub`, and `subn` hot paths, with new concurrency and GC-cycle regression coverage plus exact fallbacks for subclasses, legacy backends, and non-default arguments. 🛡️🧵🚀⚡
28
+ * 08/25/2026 **[0.6.2](https://github.com/ModelCloud/PyPcre/releases/tag/v0.6.2)**: Hardened packaging metadata and fixed FreeBSD/WSL CI failures on PCRE2 10.42 runtimes, including legacy replacement-template and JIT-fallback compatibility. 🛠️🐧🪟
29
+ * 08/25/2026 **[0.6.1](https://github.com/ModelCloud/PyPcre/releases/tag/v0.6.1)**: Workaround for PCRE2 10.46/10.47 JIT start-optimization regressions, new verifying clobber suite, second memory/thread-safety sweep, Python 3.15 `sre_parse` removal compatibility, aligned `Pattern` `pos`/`endpos` signatures, and `setuptools` metadata fixes. 🛡️🧵🐍
30
+ * 08/09/2026 **[0.6.0](https://github.com/ModelCloud/PyPcre/releases/tag/v0.6.0)**: `findall`, `finditer`, `sub`/`subn`, `split`, and `match`/`search`/`fullmatch` hot-path speedups up to **46x** vs `stdlib.re` and **48x** vs `regex`, with free-threaded `findall` reaching **13.8x** on 8 threads. 🚀⚡
29
31
  * 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. 🗂️⚡
30
32
  * 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. 🛡️⚡
31
33
  * 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. ⚡
@@ -60,7 +62,7 @@ PyPcre pairs Python's familiar `re`-compatible API with the real `PCRE2` engine.
60
62
  | Syntax power | Very rich ✅ | More limited | Rich, but different from `PCRE2` |
61
63
  | JIT execution | `PCRE2` JIT ✅ | No | No |
62
64
  | `re`-compatible API surface | Stable and familiar ✅ | Native | Similar, but not the main goal |
63
- | Free-threaded support | Built and tested for `PYTHON_GIL=0` ✅ | No explicit PyPcre-style layer | Not a project focus here |
65
+ | Free-threaded support | Built and tested for `PYTHON_GIL=0` ✅ | No explicit free-threaded support | No explicit free-threaded fan-out layer |
64
66
  | Built-in threaded subject fan-out | `parallel_map()` ✅ | No | No |
65
67
  | System library updates | Uses system `libpcre2-8` by default ✅ | N/A | N/A |
66
68
 
@@ -229,8 +231,10 @@ numbers = pattern.findall(b"line 1\nline 22")
229
231
  ### API Overview 🧭
230
232
 
231
233
  - Module helpers: `prefixmatch`, `match`, `search`, `fullmatch`, `finditer`,
232
- `findall`, `split`, `sub`, `subn`, `compile`, `escape`, `purge`, and
233
- `parallel_map`.
234
+ `findall`, `split`, `sub`, `subn`, `compile`, `escape`, `purge`,
235
+ `template`, `parallel_map`, `configure`, `configure_threads`,
236
+ `configure_thread_pool`, `shutdown_thread_pool`, `set_cache_limit`,
237
+ `get_cache_limit`, and `clear_cache`.
234
238
  - `compile()` returns a `Pattern` object with the familiar matching helpers
235
239
  plus `split()`, `sub()`, and `subn()`.
236
240
  - `Pattern` exposes `.pattern`, `.flags`, `.jit`, `.groupindex`, and `.groups`
@@ -246,7 +250,7 @@ numbers = pattern.findall(b"line 1\nline 22")
246
250
 
247
251
  ### Common examples 🧪
248
252
 
249
- Compiled patterns:
253
+ 🧪 Compiled patterns:
250
254
 
251
255
  ```python
252
256
  from pcre import compile, Flag
@@ -256,7 +260,7 @@ match = pattern.search("User: alice")
256
260
  print(match.group("name")) # alice
257
261
  ```
258
262
 
259
- Substitution:
263
+ 🔁 Substitution:
260
264
 
261
265
  ```python
262
266
  from pcre import sub
@@ -265,7 +269,7 @@ result = sub(r"\d+", "#", "room 101")
265
269
  print(result) # room #
266
270
  ```
267
271
 
268
- Bytes:
272
+ 🗂️ Bytes:
269
273
 
270
274
  ```python
271
275
  from pcre import compile
@@ -330,9 +334,9 @@ the conversion without repeating the flag.
330
334
 
331
335
  ### Optional runtime controls 🎛️
332
336
 
333
- - `pcre.configure(jit=False)` disables JIT globally. `Flag.JIT` and
337
+ - ⚙️ `pcre.configure(jit=False)` disables JIT globally. `Flag.JIT` and
334
338
  `Flag.NO_JIT` let you override that per pattern.
335
- - `pcre.set_cache_limit()`, `pcre.get_cache_limit()`, and `pcre.clear_cache()`
339
+ - 🧹 `pcre.set_cache_limit()`, `pcre.get_cache_limit()`, and `pcre.clear_cache()`
336
340
  control every high-level compile/template helper cache in the active context.
337
341
  A zero limit disables them, and `None` uses a 256-entry hard safety ceiling
338
342
  rather than permitting unbounded growth. High-level cache entries never cross
@@ -340,7 +344,7 @@ the conversion without repeating the flag.
340
344
  workers' high-level helper caches on their next cache-backed call. Backend
341
345
  scratch buffers remain thread-scoped and are released when that thread exits.
342
346
  Oversized patterns and templates are not retained.
343
- - `pcre.configure_threads()`, `pcre.configure_thread_pool()`,
347
+ - 🧵 `pcre.configure_threads()`, `pcre.configure_thread_pool()`,
344
348
  `shutdown_thread_pool()`, `Flag.THREADS`, and `Flag.NO_THREADS` are available
345
349
  if you want to opt into or restrict threaded execution.
346
350
 
@@ -64,6 +64,26 @@ from .threads import (
64
64
  )
65
65
 
66
66
  _CPattern = _pcre2.Pattern
67
+ _C_PATTERN_TYPE = _CPattern
68
+ # Method descriptors avoid repeated bound-method lookup on the dominant
69
+ # default-argument paths. The exact-type guards below keep legacy backends,
70
+ # subclasses, and monkeypatched test doubles on the general wrapper path.
71
+ _RAW_MATCH_FAST = getattr(_C_PATTERN_TYPE, "_match_fast", None)
72
+ _RAW_SEARCH_FAST = getattr(_C_PATTERN_TYPE, "_search_fast", None)
73
+ _RAW_FULLMATCH_FAST = getattr(_C_PATTERN_TYPE, "_fullmatch_fast", None)
74
+ _RAW_FINDITER_FAST = getattr(_C_PATTERN_TYPE, "_finditer_fast", None)
75
+ _RAW_FINDALL_FAST = getattr(_C_PATTERN_TYPE, "_findall_fast", None)
76
+ _RAW_SPLIT_FAST = getattr(_C_PATTERN_TYPE, "_split_fast", None)
77
+ _RAW_SPLIT_LITERAL_CAPTURE_FAST = getattr(
78
+ _C_PATTERN_TYPE, "_split_literal_capture_fast", None
79
+ )
80
+ _RAW_SPLIT_LITERAL_CAPTURES_FAST = getattr(
81
+ _C_PATTERN_TYPE, "_split_literal_captures_fast", None
82
+ )
83
+ _RAW_SUBSTITUTE_FAST = getattr(_C_PATTERN_TYPE, "_substitute_fast", None)
84
+ _RAW_SUBSTITUTE_PYTHON_FAST = getattr(
85
+ _C_PATTERN_TYPE, "_substitute_python_fast", None
86
+ )
67
87
  _ORIGINAL_CACHED_COMPILE = cached_compile
68
88
  PcreError = _pcre2.PcreError
69
89
  Match = getattr(_pcre2, "Match", _CompatMatch)
@@ -101,6 +121,10 @@ def _synchronize_local_caches() -> None:
101
121
  setattr(_DEFAULT_COMPILE_LOCAL, name, None)
102
122
  _DEFAULT_COMPILE_LOCAL.module_hot_key = None
103
123
  _DEFAULT_COMPILE_LOCAL.module_hot_value = None
124
+ _DEFAULT_COMPILE_LOCAL.module_hot_pattern = None
125
+ _DEFAULT_COMPILE_LOCAL.module_hot_config = None
126
+ _DEFAULT_COMPILE_LOCAL.module_hot_thread_default = None
127
+ _DEFAULT_COMPILE_LOCAL.module_hot_epoch = -1
104
128
  _DEFAULT_COMPILE_LOCAL.replacement_hot_key = None
105
129
  _DEFAULT_COMPILE_LOCAL.replacement_hot_value = None
106
130
  for name in _LOCAL_CACHE_NAMES:
@@ -138,6 +162,10 @@ def _cache_configuration_changed() -> None:
138
162
  setattr(_DEFAULT_COMPILE_LOCAL, name, None)
139
163
  _DEFAULT_COMPILE_LOCAL.module_hot_key = None
140
164
  _DEFAULT_COMPILE_LOCAL.module_hot_value = None
165
+ _DEFAULT_COMPILE_LOCAL.module_hot_pattern = None
166
+ _DEFAULT_COMPILE_LOCAL.module_hot_config = None
167
+ _DEFAULT_COMPILE_LOCAL.module_hot_thread_default = None
168
+ _DEFAULT_COMPILE_LOCAL.module_hot_epoch = -1
141
169
  _DEFAULT_COMPILE_LOCAL.replacement_hot_key = None
142
170
  _DEFAULT_COMPILE_LOCAL.replacement_hot_value = None
143
171
  _DEFAULT_COMPILE_LOCAL.effective_limit = get_effective_cache_limit()
@@ -548,6 +576,17 @@ class Pattern:
548
576
  if type(subject) is memoryview:
549
577
  subject = subject.tobytes()
550
578
  if self._is_c_pattern:
579
+ if (
580
+ _RAW_MATCH_FAST is not None
581
+ and type(self) is Pattern
582
+ and type(self._pattern) is _C_PATTERN_TYPE
583
+ and type(pos) is int
584
+ and pos == 0
585
+ and endpos is None
586
+ and type(options) is int
587
+ and options == 0
588
+ ):
589
+ return _RAW_MATCH_FAST(self._pattern, subject, self)
551
590
  compiled_end = resolve_endpos(subject, endpos) if endpos is not None else -1
552
591
  return self._pattern.match(subject, pos, compiled_end, options, self)
553
592
  if endpos is None:
@@ -575,6 +614,17 @@ class Pattern:
575
614
  if type(subject) is memoryview:
576
615
  subject = subject.tobytes()
577
616
  if self._is_c_pattern:
617
+ if (
618
+ _RAW_SEARCH_FAST is not None
619
+ and type(self) is Pattern
620
+ and type(self._pattern) is _C_PATTERN_TYPE
621
+ and type(pos) is int
622
+ and pos == 0
623
+ and endpos is None
624
+ and type(options) is int
625
+ and options == 0
626
+ ):
627
+ return _RAW_SEARCH_FAST(self._pattern, subject, self)
578
628
  compiled_end = resolve_endpos(subject, endpos) if endpos is not None else -1
579
629
  return self._pattern.search(subject, pos, compiled_end, options, self)
580
630
  if endpos is None:
@@ -620,6 +670,17 @@ class Pattern:
620
670
  if type(subject) is memoryview:
621
671
  subject = subject.tobytes()
622
672
  if self._is_c_pattern:
673
+ if (
674
+ _RAW_FULLMATCH_FAST is not None
675
+ and type(self) is Pattern
676
+ and type(self._pattern) is _C_PATTERN_TYPE
677
+ and type(pos) is int
678
+ and pos == 0
679
+ and endpos is None
680
+ and type(options) is int
681
+ and options == 0
682
+ ):
683
+ return _RAW_FULLMATCH_FAST(self._pattern, subject, self)
623
684
  compiled_end = resolve_endpos(subject, endpos) if endpos is not None else -1
624
685
  return self._pattern.fullmatch(subject, pos, compiled_end, options, self)
625
686
  if endpos is None:
@@ -644,6 +705,27 @@ class Pattern:
644
705
  ) -> Iterator[Match]:
645
706
  if type(subject) is memoryview:
646
707
  subject = subject.tobytes()
708
+ if (
709
+ _RAW_FINDITER_FAST is not None
710
+ and self._is_c_pattern
711
+ and type(self) is Pattern
712
+ and type(self._pattern) is _C_PATTERN_TYPE
713
+ and type(pos) is int
714
+ and pos == 0
715
+ and endpos is None
716
+ and type(options) is int
717
+ and options == 0
718
+ ):
719
+ return _RAW_FINDITER_FAST(self._pattern, subject, 0, -1, 0, self)
720
+ return self._finditer_general(subject, pos, endpos, options)
721
+
722
+ def _finditer_general(
723
+ self,
724
+ subject: Any,
725
+ pos: int,
726
+ endpos: int | None,
727
+ options: int,
728
+ ) -> Iterator[Match]:
647
729
  resolved_end = resolve_endpos(subject, endpos)
648
730
  backend_iter = getattr(self._pattern, "finditer", None)
649
731
  if backend_iter is not None:
@@ -783,6 +865,11 @@ class Pattern:
783
865
  and type(options) is int
784
866
  and options == 0
785
867
  ):
868
+ if (
869
+ type(self._pattern) is _C_PATTERN_TYPE
870
+ and _RAW_FINDALL_FAST is not None
871
+ ):
872
+ return _RAW_FINDALL_FAST(self._pattern, subject)
786
873
  fast = getattr(self._pattern, "_findall_fast", None)
787
874
  if fast is not None:
788
875
  return fast(subject)
@@ -846,9 +933,11 @@ class Pattern:
846
933
  and literal_capture is not None
847
934
  and type(subject) is type(literal_capture)
848
935
  and type(maxsplit) is int
936
+ and type(self._pattern) is _C_PATTERN_TYPE
937
+ and _RAW_SPLIT_LITERAL_CAPTURE_FAST is not None
849
938
  ):
850
- return self._pattern._split_literal_capture_fast(
851
- subject, literal_capture, maxsplit
939
+ return _RAW_SPLIT_LITERAL_CAPTURE_FAST(
940
+ self._pattern, subject, literal_capture, maxsplit
852
941
  )
853
942
 
854
943
  literal_multi = self._literal_findall_multi
@@ -858,10 +947,12 @@ class Pattern:
858
947
  and literal_multi is not None
859
948
  and type(subject) is type(literal_multi[0])
860
949
  and type(maxsplit) is int
950
+ and type(self._pattern) is _C_PATTERN_TYPE
951
+ and _RAW_SPLIT_LITERAL_CAPTURES_FAST is not None
861
952
  ):
862
953
  needle, groups = literal_multi
863
- return self._pattern._split_literal_captures_fast(
864
- subject, needle, groups, maxsplit
954
+ return _RAW_SPLIT_LITERAL_CAPTURES_FAST(
955
+ self._pattern, subject, needle, groups, maxsplit
865
956
  )
866
957
 
867
958
  # The common immutable/default shape can go straight to the C splitter.
@@ -875,6 +966,11 @@ class Pattern:
875
966
  and type(maxsplit) is int
876
967
  and maxsplit == 0
877
968
  ):
969
+ if (
970
+ type(self._pattern) is _C_PATTERN_TYPE
971
+ and _RAW_SPLIT_FAST is not None
972
+ ):
973
+ return _RAW_SPLIT_FAST(self._pattern, subject, 0)
878
974
  fast_split = getattr(self._pattern, "_split_fast", None)
879
975
  if fast_split is not None:
880
976
  return fast_split(subject, 0)
@@ -946,6 +1042,42 @@ class Pattern:
946
1042
  return parts
947
1043
 
948
1044
  def sub(self, repl: Any, subject: Any, count: Any = 0) -> Any:
1045
+ if (
1046
+ _RAW_SUBSTITUTE_FAST is not None
1047
+ and self._is_c_pattern
1048
+ and type(self) is Pattern
1049
+ and type(self._pattern) is _C_PATTERN_TYPE
1050
+ and self._literal_split is None
1051
+ and type(subject) in (str, bytes)
1052
+ and type(repl) is type(subject)
1053
+ and type(count) is int
1054
+ and 0 <= count <= 8
1055
+ ):
1056
+ replacement_is_text = type(repl) is str
1057
+ has_escape = (
1058
+ "\\" in repl if replacement_is_text else b"\\" in repl
1059
+ )
1060
+ has_dollar = "$" in repl if replacement_is_text else b"$" in repl
1061
+ if not has_escape and not has_dollar:
1062
+ if count == 0:
1063
+ return _RAW_SUBSTITUTE_FAST(self._pattern, subject, repl)[0]
1064
+ return _RAW_SUBSTITUTE_FAST(
1065
+ self._pattern, subject, repl, count
1066
+ )[0]
1067
+ if (
1068
+ has_escape
1069
+ and not has_dollar
1070
+ and _RAW_SUBSTITUTE_PYTHON_FAST is not None
1071
+ ):
1072
+ direct = (
1073
+ _RAW_SUBSTITUTE_PYTHON_FAST(self._pattern, subject, repl)
1074
+ if count == 0
1075
+ else _RAW_SUBSTITUTE_PYTHON_FAST(
1076
+ self._pattern, subject, repl, count
1077
+ )
1078
+ )
1079
+ if direct is not NotImplemented:
1080
+ return direct[0]
949
1081
  result, _ = self.subn(repl, subject, count)
950
1082
  return result
951
1083
 
@@ -1528,6 +1660,10 @@ def _clear_module_cache() -> None:
1528
1660
  _DEFAULT_COMPILE_LOCAL.module_lru = None
1529
1661
  _DEFAULT_COMPILE_LOCAL.module_hot_key = None
1530
1662
  _DEFAULT_COMPILE_LOCAL.module_hot_value = None
1663
+ _DEFAULT_COMPILE_LOCAL.module_hot_pattern = None
1664
+ _DEFAULT_COMPILE_LOCAL.module_hot_config = None
1665
+ _DEFAULT_COMPILE_LOCAL.module_hot_thread_default = None
1666
+ _DEFAULT_COMPILE_LOCAL.module_hot_epoch = -1
1531
1667
 
1532
1668
 
1533
1669
  def _module_cache_size() -> int:
@@ -1539,26 +1675,41 @@ _cached_module_pattern.cache_clear = _clear_module_cache # type: ignore[attr-de
1539
1675
 
1540
1676
 
1541
1677
  def _module_compile(pattern: Any, flags: FlagInput) -> Pattern:
1542
- if type(pattern) in (str, bytes) and type(flags) in (int, Flag) and flags == 0:
1543
- default_jit, default_compat = _default_config_snapshot()
1678
+ if (
1679
+ (type(pattern) is str or type(pattern) is bytes)
1680
+ and (type(flags) is int or type(flags) is Flag)
1681
+ and flags == 0
1682
+ ):
1683
+ state = _DEFAULT_COMPILE_LOCAL
1684
+ thread_default = get_thread_default()
1685
+ try:
1686
+ if (
1687
+ state.module_hot_pattern is pattern
1688
+ and state.module_hot_config is _DEFAULT_CONFIG
1689
+ and state.module_hot_thread_default == thread_default
1690
+ and state.module_hot_epoch == get_cache_epoch()
1691
+ ):
1692
+ return state.module_hot_value
1693
+ except AttributeError:
1694
+ pass
1695
+ default_jit, default_compat = _DEFAULT_CONFIG
1544
1696
  thread_mode = (
1545
- _THREAD_MODE_AUTO if get_thread_default() else _THREAD_MODE_DISABLED
1697
+ _THREAD_MODE_AUTO if thread_default else _THREAD_MODE_DISABLED
1546
1698
  )
1547
1699
  key = (pattern, default_jit, default_compat, thread_mode)
1548
- if (
1549
- getattr(_DEFAULT_COMPILE_LOCAL, "epoch", -1) == get_cache_epoch()
1550
- and getattr(_DEFAULT_COMPILE_LOCAL, "module_hot_key", None) == key
1551
- ):
1552
- return _DEFAULT_COMPILE_LOCAL.module_hot_value
1553
1700
  compiled = _cached_module_pattern(
1554
1701
  pattern,
1555
1702
  default_jit,
1556
1703
  default_compat,
1557
1704
  thread_mode,
1558
1705
  )
1559
- if _DEFAULT_COMPILE_LOCAL.effective_limit > 0 and cache_input_allowed(pattern):
1560
- _DEFAULT_COMPILE_LOCAL.module_hot_key = key
1561
- _DEFAULT_COMPILE_LOCAL.module_hot_value = compiled
1706
+ if state.effective_limit > 0 and cache_input_allowed(pattern):
1707
+ state.module_hot_key = key
1708
+ state.module_hot_value = compiled
1709
+ state.module_hot_pattern = pattern
1710
+ state.module_hot_config = _DEFAULT_CONFIG
1711
+ state.module_hot_thread_default = thread_default
1712
+ state.module_hot_epoch = get_cache_epoch()
1562
1713
  return compiled
1563
1714
  return compile(pattern, flags=flags)
1564
1715
 
@@ -1572,13 +1723,42 @@ def _module_lookup(pattern: Any, string: Any, flags: FlagInput, method: str) ->
1572
1723
  and type(string) in (str, bytes)
1573
1724
  and compiled._is_c_pattern
1574
1725
  ):
1575
- if (
1576
- method == "findall"
1577
- and getattr(compiled, "_literal_split", None) is not None
1578
- and type(string) is type(compiled._literal_split)
1579
- ):
1580
- return compiled.findall(string)
1581
- fast = getattr(compiled._pattern, f"_{method}_fast", None)
1726
+ if method == "findall":
1727
+ literal_capture = getattr(compiled, "_literal_findall", None)
1728
+ if (
1729
+ type(compiled) is Pattern
1730
+ and literal_capture is not None
1731
+ and type(string) is type(literal_capture)
1732
+ ):
1733
+ return [literal_capture] * string.count(literal_capture)
1734
+ literal_multi = getattr(compiled, "_literal_findall_multi", None)
1735
+ if (
1736
+ type(compiled) is Pattern
1737
+ and literal_multi is not None
1738
+ and type(string) is type(literal_multi[0])
1739
+ ):
1740
+ needle, groups = literal_multi
1741
+ return [groups] * string.count(needle)
1742
+ literal_split = getattr(compiled, "_literal_split", None)
1743
+ if (
1744
+ type(compiled) is Pattern
1745
+ and literal_split is not None
1746
+ and type(string) is type(literal_split)
1747
+ ):
1748
+ return [literal_split] * string.count(literal_split)
1749
+ raw_pattern = compiled._pattern
1750
+ if type(raw_pattern) is _C_PATTERN_TYPE:
1751
+ if method == "findall" and _RAW_FINDALL_FAST is not None:
1752
+ return _RAW_FINDALL_FAST(raw_pattern, string)
1753
+ if method == "finditer" and _RAW_FINDITER_FAST is not None:
1754
+ return _RAW_FINDITER_FAST(raw_pattern, string, 0, -1, 0, compiled)
1755
+ if method == "match" and _RAW_MATCH_FAST is not None:
1756
+ return _RAW_MATCH_FAST(raw_pattern, string, compiled)
1757
+ if method == "search" and _RAW_SEARCH_FAST is not None:
1758
+ return _RAW_SEARCH_FAST(raw_pattern, string, compiled)
1759
+ if method == "fullmatch" and _RAW_FULLMATCH_FAST is not None:
1760
+ return _RAW_FULLMATCH_FAST(raw_pattern, string, compiled)
1761
+ fast = getattr(raw_pattern, f"_{method}_fast", None)
1582
1762
  if fast is not None:
1583
1763
  if method == "findall":
1584
1764
  return fast(string)
@@ -1626,7 +1806,41 @@ def split(
1626
1806
  literal_split = getattr(compiled, "_literal_split", None)
1627
1807
  if literal_split is not None and type(string) is type(literal_split):
1628
1808
  return compiled.split(string, maxsplit)
1629
- fast = getattr(compiled._pattern, "_split_fast", None)
1809
+ literal_capture = getattr(compiled, "_literal_findall", None)
1810
+ if (
1811
+ type(compiled) is Pattern
1812
+ and literal_capture is not None
1813
+ and type(string) is type(literal_capture)
1814
+ ):
1815
+ raw_pattern = compiled._pattern
1816
+ if (
1817
+ type(raw_pattern) is _C_PATTERN_TYPE
1818
+ and _RAW_SPLIT_LITERAL_CAPTURE_FAST is not None
1819
+ ):
1820
+ return _RAW_SPLIT_LITERAL_CAPTURE_FAST(
1821
+ raw_pattern, string, literal_capture, maxsplit
1822
+ )
1823
+ return compiled.split(string, maxsplit)
1824
+ literal_multi = getattr(compiled, "_literal_findall_multi", None)
1825
+ if (
1826
+ type(compiled) is Pattern
1827
+ and literal_multi is not None
1828
+ and type(string) is type(literal_multi[0])
1829
+ ):
1830
+ needle, groups = literal_multi
1831
+ raw_pattern = compiled._pattern
1832
+ if (
1833
+ type(raw_pattern) is _C_PATTERN_TYPE
1834
+ and _RAW_SPLIT_LITERAL_CAPTURES_FAST is not None
1835
+ ):
1836
+ return _RAW_SPLIT_LITERAL_CAPTURES_FAST(
1837
+ raw_pattern, string, needle, groups, maxsplit
1838
+ )
1839
+ return compiled.split(string, maxsplit)
1840
+ raw_pattern = compiled._pattern
1841
+ if type(raw_pattern) is _C_PATTERN_TYPE and _RAW_SPLIT_FAST is not None:
1842
+ return _RAW_SPLIT_FAST(raw_pattern, string, maxsplit)
1843
+ fast = getattr(raw_pattern, "_split_fast", None)
1630
1844
  if fast is not None:
1631
1845
  return fast(string, maxsplit)
1632
1846
  return compiled.split(string, maxsplit=maxsplit)
@@ -1635,7 +1849,13 @@ def split(
1635
1849
  def sub(
1636
1850
  pattern: Any, repl: Any, string: Any, count: Any = 0, flags: FlagInput = 0
1637
1851
  ) -> Any:
1638
- result = subn(pattern, repl, string, count=count, flags=flags)
1852
+ compiled = _module_compile(pattern, flags)
1853
+ if (
1854
+ type(compiled) is Pattern
1855
+ and type(compiled._pattern) is _C_PATTERN_TYPE
1856
+ ):
1857
+ return compiled.sub(repl, string, count)
1858
+ result = compiled.subn(repl, string, count=count)
1639
1859
  return result[0]
1640
1860
 
1641
1861
 
@@ -30,6 +30,7 @@ resolve_pcre2_prerelease(void)
30
30
  /* Process-wide library metadata cached once during module initialization. */
31
31
  static char pcre2_library_version[64] = "unknown";
32
32
  static ATOMIC_VAR(int) pcre2_version_initialized = 0;
33
+ static atomic_flag pcre2_version_lock = ATOMIC_FLAG_INIT;
33
34
  /* Module execution is limited to one interpreter because these are process globals. */
34
35
  static ATOMIC_VAR(PyInterpreterState *) primary_interpreter = ATOMIC_VAR_INIT(NULL);
35
36
  #if defined(PCRE2_USE_OFFSET_LIMIT)
@@ -1024,24 +1025,34 @@ Match_get_lastindex(MatchObject *self, void *closure)
1024
1025
  return PyLong_FromLong(lastindex);
1025
1026
  }
1026
1027
 
1028
+ /*
1029
+ * Replay compiles/runs PCRE2 and may allocate Python objects, so it must
1030
+ * not run while holding the Match critical section. Concurrent callers
1031
+ * may replay the same immutable snapshot; the first successful result is
1032
+ * published under the lock and all callers use that cached value.
1033
+ */
1027
1034
  int lastindex = -1;
1028
- int replay_ok = 0;
1029
- Py_BEGIN_CRITICAL_SECTION(self);
1030
- if (self->lastindex_cache == -2) {
1031
- int replayed = 0;
1032
- if (match_replay_lastindex(self, &replayed) == 0) {
1033
- self->lastindex_cache = replayed == 0 ? -1 : replayed;
1034
- replay_ok = 1;
1035
+ int replayed = 0;
1036
+ if (match_replay_lastindex(self, &replayed) < 0) {
1037
+ int cached_lastindex = -2;
1038
+ Py_BEGIN_CRITICAL_SECTION(self);
1039
+ cached_lastindex = self->lastindex_cache;
1040
+ Py_END_CRITICAL_SECTION();
1041
+ if (cached_lastindex == -2) {
1042
+ return NULL;
1035
1043
  }
1044
+ /* Another thread completed the replay while this one failed. */
1045
+ PyErr_Clear();
1046
+ lastindex = cached_lastindex;
1036
1047
  } else {
1037
- replay_ok = 1;
1048
+ Py_BEGIN_CRITICAL_SECTION(self);
1049
+ if (self->lastindex_cache == -2) {
1050
+ self->lastindex_cache = replayed == 0 ? -1 : replayed;
1051
+ }
1052
+ lastindex = self->lastindex_cache;
1053
+ Py_END_CRITICAL_SECTION();
1038
1054
  }
1039
- lastindex = self->lastindex_cache;
1040
- Py_END_CRITICAL_SECTION();
1041
1055
 
1042
- if (!replay_ok) {
1043
- return NULL;
1044
- }
1045
1056
  if (lastindex < 0) {
1046
1057
  Py_RETURN_NONE;
1047
1058
  }
@@ -6434,7 +6445,7 @@ module_exec(PyObject *module)
6434
6445
  goto error_cache;
6435
6446
  }
6436
6447
 
6437
- if (PyModule_AddStringConstant(module, "__version__", "0.6.1") < 0) {
6448
+ if (PyModule_AddStringConstant(module, "__version__", "0.6.3") < 0) {
6438
6449
  goto error_cache;
6439
6450
  }
6440
6451
 
@@ -6512,6 +6523,16 @@ initialize_pcre2_version(void)
6512
6523
  return;
6513
6524
  }
6514
6525
 
6526
+ while (atomic_flag_test_and_set_explicit(
6527
+ &pcre2_version_lock,
6528
+ memory_order_acquire)) {
6529
+ /* Initialization only performs a few PCRE2 queries and string copies. */
6530
+ }
6531
+ if (atomic_load_explicit(&pcre2_version_initialized, memory_order_acquire)) {
6532
+ atomic_flag_clear_explicit(&pcre2_version_lock, memory_order_release);
6533
+ return;
6534
+ }
6535
+
6515
6536
  char buffer[sizeof(pcre2_library_version)] = {0};
6516
6537
  if (pcre2_config(PCRE2_CONFIG_VERSION, buffer) > 0 && buffer[0] != '\0') {
6517
6538
  strncpy(pcre2_library_version, buffer, sizeof(pcre2_library_version) - 1);
@@ -6538,4 +6559,5 @@ initialize_pcre2_version(void)
6538
6559
  }
6539
6560
  }
6540
6561
  atomic_store_explicit(&pcre2_version_initialized, 1, memory_order_release);
6562
+ atomic_flag_clear_explicit(&pcre2_version_lock, memory_order_release);
6541
6563
  }
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "PyPcre"
12
- version = "0.6.2"
12
+ version = "0.6.3"
13
13
  description = "Modern, GIL-friendly, Fast Python bindings for PCRE2 with auto caching and JIT of compiled patterns."
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.10"
@@ -310,7 +310,7 @@ EXTENSION = Extension(
310
310
 
311
311
  setup(
312
312
  name="PyPcre",
313
- version="0.6.1",
313
+ version="0.6.3",
314
314
  packages=["pcre"],
315
315
  ext_modules=[EXTENSION],
316
316
  cmdclass={"build_ext": build_ext},
@@ -5,6 +5,7 @@
5
5
  from __future__ import annotations
6
6
 
7
7
  import re
8
+ import threading
8
9
  from concurrent.futures import ThreadPoolExecutor
9
10
 
10
11
  import pcre_ext_c as raw
@@ -113,3 +114,22 @@ def test_lastindex_replay_code_initialization_is_shared_thread_safely() -> None:
113
114
  with ThreadPoolExecutor(max_workers=16) as executor:
114
115
  observed = list(executor.map(read_distinct_match, matches))
115
116
  assert observed == [(1, "outer")] * len(matches)
117
+
118
+
119
+ def test_shared_multicapture_replay_is_thread_safe() -> None:
120
+ pattern = pcre.compile(r"(?P<outer>a(?P<inner>b))", pcre.Flag.NO_JIT)
121
+ match = pattern.fullmatch("ab")
122
+ assert match is not None
123
+ barrier = threading.Barrier(16)
124
+
125
+ def read_shared_match(_: int) -> set[tuple[int | None, str | None]]:
126
+ barrier.wait(timeout=5)
127
+ observed: set[tuple[int | None, str | None]] = set()
128
+ for _ in range(512):
129
+ observed.add((match.lastindex, match.lastgroup))
130
+ assert match.re is pattern
131
+ return observed
132
+
133
+ with ThreadPoolExecutor(max_workers=16) as executor:
134
+ observed = list(executor.map(read_shared_match, range(16)))
135
+ assert observed == [{(1, "outer")}] * 16
@@ -4,8 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import gc
6
6
  import tracemalloc
7
+ import weakref
7
8
 
8
9
  import pcre
10
+ import pcre_ext_c as raw
9
11
  import pytest
10
12
  from pcre import Flag
11
13
 
@@ -159,3 +161,48 @@ def test_repeated_operation_releases_memory(method: str, flags: Flag) -> None:
159
161
 
160
162
  leaked = after - before
161
163
  assert leaked <= 16384, f"{method} leaked {leaked} bytes over 1000 iterations"
164
+
165
+
166
+ class _TextSubject(str):
167
+ pass
168
+
169
+
170
+ class _BufferSubject(bytearray):
171
+ pass
172
+
173
+
174
+ def _assert_collected(ref: weakref.ReferenceType[object]) -> None:
175
+ for _ in range(5):
176
+ gc.collect()
177
+ if ref() is None:
178
+ return
179
+ assert ref() is None
180
+
181
+
182
+ @pytest.mark.parametrize("subject_factory", [_TextSubject, _BufferSubject])
183
+ @pytest.mark.parametrize("operation", ["match", "finditer"])
184
+ def test_match_and_finditer_ownership_cycles_are_collectable(
185
+ subject_factory: type[str] | type[bytearray],
186
+ operation: str,
187
+ ) -> None:
188
+ is_text = subject_factory is _TextSubject
189
+ subject = subject_factory("abc" if is_text else b"abc")
190
+ pattern = raw.compile("(a)" if is_text else b"(a)", jit=False)
191
+ result = getattr(pattern, operation)(subject)
192
+ subject.owner = result
193
+ ref = weakref.ref(subject)
194
+
195
+ del result, pattern, subject
196
+ raw.clear_pattern_cache()
197
+ _assert_collected(ref)
198
+
199
+
200
+ def test_pattern_source_ownership_cycle_is_collectable() -> None:
201
+ subject = _TextSubject("(a)")
202
+ pattern = raw.compile(subject, jit=False)
203
+ subject.owner = pattern
204
+ ref = weakref.ref(subject)
205
+
206
+ del pattern, subject
207
+ raw.clear_pattern_cache()
208
+ _assert_collected(ref)
@@ -1,3 +1,5 @@
1
+ import concurrent.futures
2
+ import threading
1
3
  import unittest
2
4
 
3
5
  import pcre_ext_c
@@ -17,6 +19,19 @@ class PcreVersionTest(unittest.TestCase):
17
19
 
18
20
  self.assertEqual(runtime_version, version_constant)
19
21
 
22
+ def test_runtime_version_query_is_thread_safe(self) -> None:
23
+ barrier = threading.Barrier(16)
24
+
25
+ def query(_: int) -> str:
26
+ barrier.wait(timeout=5)
27
+ return pcre_ext_c.get_library_version()
28
+
29
+ with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
30
+ versions = list(executor.map(query, range(64)))
31
+
32
+ self.assertEqual(len(set(versions)), 1)
33
+ self.assertTrue(versions[0])
34
+
20
35
 
21
36
  if __name__ == "__main__":
22
37
  unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes