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.
- {pypcre-0.6.2 → pypcre-0.6.3}/PKG-INFO +17 -13
- {pypcre-0.6.2 → pypcre-0.6.3}/PyPcre.egg-info/PKG-INFO +17 -13
- {pypcre-0.6.2 → pypcre-0.6.3}/README.md +16 -12
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre/pcre.py +244 -24
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/pcre2.c +36 -14
- {pypcre-0.6.2 → pypcre-0.6.3}/pyproject.toml +1 -1
- {pypcre-0.6.2 → pypcre-0.6.3}/setup.py +1 -1
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_lastindex_semantics.py +20 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_memory.py +47 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_version.py +15 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/LICENSE +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/MANIFEST.in +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/PyPcre.egg-info/SOURCES.txt +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/PyPcre.egg-info/dependency_links.txt +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/PyPcre.egg-info/top_level.txt +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre/__init__.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre/_stdlib_re.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre/cache.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre/error.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre/flags.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre/re_compat.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre/threads.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/atomic_compat.h +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/cache.c +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/error.c +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/flag.c +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/jit_support.c +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/memory.c +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/pattern_cache.c +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/pcre2.h +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/pcre2_module.h +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/string_helpers.c +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/pcre_ext/util.c +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/setup.cfg +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/setup_utils.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_accuracy.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_api_parity.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_basic.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_bench_string.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_benchmark.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_bytes.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_c_api_audit.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_cache.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_cache_global.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_cache_global_inproc.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_cache_scope_safety.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_clobber.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_clobber_thread.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_clobber_verify.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_compile_utf_no_check_safety.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_core.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_coverage_gaps.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_errors.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_escape_fastpath.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_expand_fastpath.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_flags.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_gil_zero_safety.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_jit.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_jit_anchoring.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_jit_start_optimize_workaround.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_lastindex_ovector_fastpath.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_literal_capture_findall_fastpath.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_literal_capture_split_fastpath.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_mmap.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_module.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_multi_literal_capture_findall_fastpath.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_multi_literal_capture_split_fastpath.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_pattern.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_python_coverage_audit.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_re_compat.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_setup_utils_multiarch.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_simd.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_stdlib_flag_fastpath.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_sub_count_one_fastpath.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_sub_reference_fastpath.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_template_dispatch_fastpath.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_threaded_backend.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_threads.py +0 -0
- {pypcre-0.6.2 → pypcre-0.6.3}/tests/test_transformers_regex.py +0 -0
- {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.
|
|
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/
|
|
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
|
-
*
|
|
51
|
-
* 08/
|
|
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
|
|
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`,
|
|
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.
|
|
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/
|
|
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
|
-
*
|
|
51
|
-
* 08/
|
|
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
|
|
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`,
|
|
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/
|
|
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
|
-
*
|
|
28
|
-
* 08/
|
|
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
|
|
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`,
|
|
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
|
|
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
|
|
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
|
|
1543
|
-
|
|
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
|
|
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
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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"
|
|
@@ -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
|
|
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
|