PyPcre 0.6.1__tar.gz → 0.6.2__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.1 → pypcre-0.6.2}/PKG-INFO +1 -1
- {pypcre-0.6.1 → pypcre-0.6.2}/PyPcre.egg-info/PKG-INFO +1 -1
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre/pcre.py +10 -5
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/pcre2.c +80 -16
- {pypcre-0.6.1 → pypcre-0.6.2}/pyproject.toml +1 -1
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_clobber_verify.py +43 -3
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_coverage_gaps.py +2 -2
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_python_coverage_audit.py +3 -2
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_threaded_backend.py +1 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_threads.py +12 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/LICENSE +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/MANIFEST.in +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/PyPcre.egg-info/SOURCES.txt +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/PyPcre.egg-info/dependency_links.txt +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/PyPcre.egg-info/top_level.txt +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/README.md +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre/__init__.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre/_stdlib_re.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre/cache.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre/error.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre/flags.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre/re_compat.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre/threads.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/atomic_compat.h +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/cache.c +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/error.c +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/flag.c +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/jit_support.c +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/memory.c +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/pattern_cache.c +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/pcre2.h +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/pcre2_module.h +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/string_helpers.c +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/util.c +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/setup.cfg +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/setup.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/setup_utils.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_accuracy.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_api_parity.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_basic.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_bench_string.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_benchmark.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_bytes.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_c_api_audit.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_cache.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_cache_global.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_cache_global_inproc.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_cache_scope_safety.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_clobber.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_clobber_thread.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_compile_utf_no_check_safety.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_core.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_errors.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_escape_fastpath.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_expand_fastpath.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_flags.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_gil_zero_safety.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_jit.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_jit_anchoring.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_jit_start_optimize_workaround.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_lastindex_ovector_fastpath.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_lastindex_semantics.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_literal_capture_findall_fastpath.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_literal_capture_split_fastpath.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_memory.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_mmap.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_module.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_multi_literal_capture_findall_fastpath.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_multi_literal_capture_split_fastpath.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_pattern.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_re_compat.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_setup_utils_multiarch.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_simd.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_stdlib_flag_fastpath.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_sub_count_one_fastpath.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_sub_reference_fastpath.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_template_dispatch_fastpath.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_transformers_regex.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_utf8.py +0 -0
- {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_version.py +0 -0
|
@@ -268,7 +268,12 @@ def _normalise_flags(flags: FlagInput) -> int:
|
|
|
268
268
|
|
|
269
269
|
|
|
270
270
|
def _pcre2_replacement_from_parsed(parsed: Any, is_bytes: bool) -> Any:
|
|
271
|
-
"""Convert a parsed Python replacement template to a PCRE2 replacement string.
|
|
271
|
+
"""Convert a parsed Python replacement template to a PCRE2 replacement string.
|
|
272
|
+
|
|
273
|
+
Group references are emitted as ``${n}``: PCRE2 only accepts ``\\g<n>`` in
|
|
274
|
+
replacement strings from 10.44 onwards, while ``${n}`` works on every
|
|
275
|
+
supported runtime. Literal text is escaped so ``$`` and ``\\`` stay literal.
|
|
276
|
+
"""
|
|
272
277
|
|
|
273
278
|
if (
|
|
274
279
|
isinstance(parsed, tuple)
|
|
@@ -282,7 +287,7 @@ def _pcre2_replacement_from_parsed(parsed: Any, is_bytes: bool) -> Any:
|
|
|
282
287
|
parts = []
|
|
283
288
|
for i, lit in enumerate(literals):
|
|
284
289
|
if i in slot_to_group:
|
|
285
|
-
parts.append(("
|
|
290
|
+
parts.append(("${" + str(slot_to_group[i]) + "}").encode("ascii"))
|
|
286
291
|
if lit is not None:
|
|
287
292
|
parts.append(lit.replace(b"\\", b"\\\\").replace(b"$", b"$$"))
|
|
288
293
|
return b"".join(parts)
|
|
@@ -290,7 +295,7 @@ def _pcre2_replacement_from_parsed(parsed: Any, is_bytes: bool) -> Any:
|
|
|
290
295
|
parts = []
|
|
291
296
|
for i, lit in enumerate(literals):
|
|
292
297
|
if i in slot_to_group:
|
|
293
|
-
parts.append("
|
|
298
|
+
parts.append("${" + str(slot_to_group[i]) + "}")
|
|
294
299
|
if lit is not None:
|
|
295
300
|
parts.append(lit.replace("\\", "\\\\").replace("$", "$$"))
|
|
296
301
|
return "".join(parts)
|
|
@@ -299,7 +304,7 @@ def _pcre2_replacement_from_parsed(parsed: Any, is_bytes: bool) -> Any:
|
|
|
299
304
|
parts = []
|
|
300
305
|
for item in parsed:
|
|
301
306
|
if isinstance(item, int):
|
|
302
|
-
parts.append(("
|
|
307
|
+
parts.append(("${" + str(item) + "}").encode("ascii"))
|
|
303
308
|
else:
|
|
304
309
|
parts.append(item.replace(b"\\", b"\\\\").replace(b"$", b"$$"))
|
|
305
310
|
return b"".join(parts)
|
|
@@ -307,7 +312,7 @@ def _pcre2_replacement_from_parsed(parsed: Any, is_bytes: bool) -> Any:
|
|
|
307
312
|
parts = []
|
|
308
313
|
for item in parsed:
|
|
309
314
|
if isinstance(item, int):
|
|
310
|
-
parts.append("
|
|
315
|
+
parts.append("${" + str(item) + "}")
|
|
311
316
|
else:
|
|
312
317
|
parts.append(item.replace("\\", "\\\\").replace("$", "$$"))
|
|
313
318
|
return "".join(parts)
|
|
@@ -5188,12 +5188,17 @@ pattern_translate_single_replacement(PatternObject *self,
|
|
|
5188
5188
|
) >= 0) {
|
|
5189
5189
|
return NULL;
|
|
5190
5190
|
}
|
|
5191
|
-
|
|
5191
|
+
/* Emit PCRE2's ${n} form: \g<n> in replacement strings is only
|
|
5192
|
+
* understood by PCRE2 >= 10.44 and raises BADREPESCAPE on older
|
|
5193
|
+
* runtimes (Ubuntu 24.04 ships 10.42). ${n} has been valid since
|
|
5194
|
+
* 10.00 and the surrounding literal text is guaranteed to contain
|
|
5195
|
+
* no '$' by the checks above. */
|
|
5196
|
+
if (length > PY_SSIZE_T_MAX - 2) {
|
|
5192
5197
|
PyErr_NoMemory();
|
|
5193
5198
|
return NULL;
|
|
5194
5199
|
}
|
|
5195
5200
|
*handled = 1;
|
|
5196
|
-
Py_ssize_t result_length = length +
|
|
5201
|
+
Py_ssize_t result_length = length + 2;
|
|
5197
5202
|
if (replacement_is_bytes) {
|
|
5198
5203
|
PyObject *result = PyBytes_FromStringAndSize(NULL, result_length);
|
|
5199
5204
|
if (result == NULL) {
|
|
@@ -5202,12 +5207,11 @@ pattern_translate_single_replacement(PatternObject *self,
|
|
|
5202
5207
|
char *output = PyBytes_AS_STRING(result);
|
|
5203
5208
|
const char *input = PyBytes_AS_STRING(replacement);
|
|
5204
5209
|
memcpy(output, input, (size_t)slash_index);
|
|
5205
|
-
output[slash_index] = '
|
|
5206
|
-
output[slash_index + 1] = '
|
|
5207
|
-
output[slash_index + 2] =
|
|
5208
|
-
output[slash_index + 3] =
|
|
5209
|
-
output
|
|
5210
|
-
memcpy(output + slash_index + 5,
|
|
5210
|
+
output[slash_index] = '$';
|
|
5211
|
+
output[slash_index + 1] = '{';
|
|
5212
|
+
output[slash_index + 2] = (char)following;
|
|
5213
|
+
output[slash_index + 3] = '}';
|
|
5214
|
+
memcpy(output + slash_index + 4,
|
|
5211
5215
|
input + slash_index + 2,
|
|
5212
5216
|
(size_t)(length - slash_index - 2));
|
|
5213
5217
|
return result;
|
|
@@ -5223,14 +5227,13 @@ pattern_translate_single_replacement(PatternObject *self,
|
|
|
5223
5227
|
PyUnicode_CopyCharacters(
|
|
5224
5228
|
result, 0, replacement, 0, slash_index
|
|
5225
5229
|
) < 0) ||
|
|
5226
|
-
PyUnicode_WriteChar(result, slash_index, '
|
|
5227
|
-
PyUnicode_WriteChar(result, slash_index + 1, '
|
|
5228
|
-
PyUnicode_WriteChar(result, slash_index + 2,
|
|
5229
|
-
PyUnicode_WriteChar(result, slash_index + 3,
|
|
5230
|
-
PyUnicode_WriteChar(result, slash_index + 4, '>') < 0 ||
|
|
5230
|
+
PyUnicode_WriteChar(result, slash_index, '$') < 0 ||
|
|
5231
|
+
PyUnicode_WriteChar(result, slash_index + 1, '{') < 0 ||
|
|
5232
|
+
PyUnicode_WriteChar(result, slash_index + 2, following) < 0 ||
|
|
5233
|
+
PyUnicode_WriteChar(result, slash_index + 3, '}') < 0 ||
|
|
5231
5234
|
(slash_index + 2 < length &&
|
|
5232
5235
|
PyUnicode_CopyCharacters(result,
|
|
5233
|
-
slash_index +
|
|
5236
|
+
slash_index + 4,
|
|
5234
5237
|
replacement,
|
|
5235
5238
|
slash_index + 2,
|
|
5236
5239
|
length - slash_index - 2) < 0)) {
|
|
@@ -5287,8 +5290,62 @@ pattern_translate_single_replacement(PatternObject *self,
|
|
|
5287
5290
|
return NULL;
|
|
5288
5291
|
}
|
|
5289
5292
|
*handled = 1;
|
|
5290
|
-
|
|
5291
|
-
|
|
5293
|
+
/* Rewrite \g<name> as ${name} (same runtime-compatibility reason as
|
|
5294
|
+
* above): "\\g<" + name + ">" (4 + n chars) becomes "${" + name + "}"
|
|
5295
|
+
* (3 + n chars), so the result is exactly one character shorter. */
|
|
5296
|
+
{
|
|
5297
|
+
Py_ssize_t result_length = length - 1;
|
|
5298
|
+
Py_ssize_t tail_start = cursor + 1;
|
|
5299
|
+
Py_ssize_t tail_length = length - tail_start;
|
|
5300
|
+
if (replacement_is_bytes) {
|
|
5301
|
+
PyObject *result = PyBytes_FromStringAndSize(NULL, result_length);
|
|
5302
|
+
if (result == NULL) {
|
|
5303
|
+
return NULL;
|
|
5304
|
+
}
|
|
5305
|
+
char *output = PyBytes_AS_STRING(result);
|
|
5306
|
+
const char *input = PyBytes_AS_STRING(replacement);
|
|
5307
|
+
memcpy(output, input, (size_t)slash_index);
|
|
5308
|
+
output[slash_index] = '$';
|
|
5309
|
+
output[slash_index + 1] = '{';
|
|
5310
|
+
memcpy(output + slash_index + 2, name, (size_t)name_length);
|
|
5311
|
+
output[slash_index + 2 + name_length] = '}';
|
|
5312
|
+
memcpy(output + slash_index + 3 + name_length,
|
|
5313
|
+
input + tail_start,
|
|
5314
|
+
(size_t)tail_length);
|
|
5315
|
+
return result;
|
|
5316
|
+
}
|
|
5317
|
+
PyObject *result = PyUnicode_New(
|
|
5318
|
+
result_length, PyUnicode_MAX_CHAR_VALUE(replacement)
|
|
5319
|
+
);
|
|
5320
|
+
if (result == NULL) {
|
|
5321
|
+
return NULL;
|
|
5322
|
+
}
|
|
5323
|
+
if ((slash_index > 0 &&
|
|
5324
|
+
PyUnicode_CopyCharacters(result, 0, replacement, 0, slash_index) < 0) ||
|
|
5325
|
+
PyUnicode_WriteChar(result, slash_index, '$') < 0 ||
|
|
5326
|
+
PyUnicode_WriteChar(result, slash_index + 1, '{') < 0) {
|
|
5327
|
+
Py_DECREF(result);
|
|
5328
|
+
return NULL;
|
|
5329
|
+
}
|
|
5330
|
+
for (Py_ssize_t i = 0; i < name_length; ++i) {
|
|
5331
|
+
if (PyUnicode_WriteChar(result, slash_index + 2 + i,
|
|
5332
|
+
(Py_UCS4)(unsigned char)name[i]) < 0) {
|
|
5333
|
+
Py_DECREF(result);
|
|
5334
|
+
return NULL;
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
if (PyUnicode_WriteChar(result, slash_index + 2 + name_length, '}') < 0 ||
|
|
5338
|
+
(tail_length > 0 &&
|
|
5339
|
+
PyUnicode_CopyCharacters(result,
|
|
5340
|
+
slash_index + 3 + name_length,
|
|
5341
|
+
replacement,
|
|
5342
|
+
tail_start,
|
|
5343
|
+
tail_length) < 0)) {
|
|
5344
|
+
Py_DECREF(result);
|
|
5345
|
+
return NULL;
|
|
5346
|
+
}
|
|
5347
|
+
return result;
|
|
5348
|
+
}
|
|
5292
5349
|
}
|
|
5293
5350
|
|
|
5294
5351
|
static PyObject *
|
|
@@ -5671,6 +5728,13 @@ Pattern_create(PyObject *pattern_obj, uint32_t options, int jit, int jit_explici
|
|
|
5671
5728
|
} else if (!jit_explicit && jit_rc == PCRE2_ERROR_JIT_UNSUPPORTED) {
|
|
5672
5729
|
pattern_jit_set(pattern, 0);
|
|
5673
5730
|
#endif
|
|
5731
|
+
} else if (!jit_explicit && jit_rc == PCRE2_ERROR_NOMEMORY) {
|
|
5732
|
+
/* PCRE2 < 10.43 has no JIT_UNSUPPORTED code and reports every
|
|
5733
|
+
* construct the JIT cannot compile (e.g. \C in UTF mode) as
|
|
5734
|
+
* NOMEMORY. For implicit JIT requests fall back to the
|
|
5735
|
+
* interpreter exactly as newer runtimes do; an explicit jit=True
|
|
5736
|
+
* still surfaces the error. */
|
|
5737
|
+
pattern_jit_set(pattern, 0);
|
|
5674
5738
|
} else {
|
|
5675
5739
|
Py_DECREF(pattern);
|
|
5676
5740
|
raise_pcre_error("jit_compile", jit_rc, 0);
|
|
@@ -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.2"
|
|
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"
|
|
@@ -50,6 +50,29 @@ _DIFF_DURATION = float(os.getenv("PYPCRE_CLOBBER_VERIFY_SECONDS", "45"))
|
|
|
50
50
|
_EXT_DURATION = max(10.0, _DIFF_DURATION * 0.6)
|
|
51
51
|
_JOIN_GRACE = 60.0
|
|
52
52
|
_MAX_DIFF_SUBJECT = 48 # short: the re oracle has no backtracking limits
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _pcre2_runtime_version() -> tuple[int, int]:
|
|
56
|
+
import pcre_ext_c
|
|
57
|
+
|
|
58
|
+
text = str(getattr(pcre_ext_c, "PCRE2_VERSION", "0.0")).split()[0]
|
|
59
|
+
major, _, minor = text.partition(".")
|
|
60
|
+
try:
|
|
61
|
+
return int(major), int(minor)
|
|
62
|
+
except ValueError:
|
|
63
|
+
return (0, 0)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Older PCRE2 runtimes have known engine-level wrong-result bugs that pypcre
|
|
67
|
+
# cannot paper over (e.g. 10.42 start-optimization loses matches for
|
|
68
|
+
# (?=2{1,3}\D?)(?:.?2){1,1}e{0,} in BOTH interpreter and JIT unless compiled
|
|
69
|
+
# with PCRE2_NO_START_OPTIMIZE; fixed by 10.46). Accuracy mismatches on such
|
|
70
|
+
# runtimes are reported rather than failed; crashes, errors and hangs still
|
|
71
|
+
# fail everywhere. PYPCRE_CLOBBER_STRICT_ENGINE=1 restores hard failures.
|
|
72
|
+
_TRUSTED_ENGINE = (
|
|
73
|
+
_pcre2_runtime_version() >= (10, 46)
|
|
74
|
+
or bool(os.getenv("PYPCRE_CLOBBER_STRICT_ENGINE"))
|
|
75
|
+
)
|
|
53
76
|
_MAX_EXT_SUBJECT = 2048
|
|
54
77
|
|
|
55
78
|
|
|
@@ -251,8 +274,11 @@ def _gen_group(ctx: _GenCtx, parent_bounded: bool) -> tuple[str, bool]:
|
|
|
251
274
|
body, body_nullable = _gen_seq(
|
|
252
275
|
ctx, bounded_only=parent_bounded or quantify, max_pieces=3
|
|
253
276
|
)
|
|
254
|
-
# Never quantify a group whose body can match empty (see _gen_piece)
|
|
255
|
-
|
|
277
|
+
# Never quantify a group whose body can match empty (see _gen_piece), and
|
|
278
|
+
# never quantify a group nested inside an already-quantified group: even
|
|
279
|
+
# with bounded ranges, stacked group quantifiers multiply the re oracle's
|
|
280
|
+
# backtracking (e.g. ((.{1,3}|0{2,4}){1,3}\1?){2,4} took 30 s per call).
|
|
281
|
+
quantify = quantify and not body_nullable and not parent_bounded
|
|
256
282
|
if not capturing:
|
|
257
283
|
if r < 0.30 or not ctx.allow_pcre_only:
|
|
258
284
|
out = f"(?:{body})"
|
|
@@ -468,11 +494,15 @@ def _differential_case(
|
|
|
468
494
|
)
|
|
469
495
|
|
|
470
496
|
def fail(op: str, got, want) -> None:
|
|
471
|
-
|
|
497
|
+
message = (
|
|
472
498
|
f"MISMATCH {op}: seed={seed} pattern={pattern_input!r} "
|
|
473
499
|
f"subject={subject!r} pos={pos} endpos={endpos} "
|
|
474
500
|
f"pcre={got!r} re={want!r}"
|
|
475
501
|
)
|
|
502
|
+
if _TRUSTED_ENGINE:
|
|
503
|
+
failures.record(message)
|
|
504
|
+
else:
|
|
505
|
+
_ENGINE_DIVERGENCES.record(message, halt=False)
|
|
476
506
|
|
|
477
507
|
try:
|
|
478
508
|
for op in ("search", "match", "fullmatch"):
|
|
@@ -543,6 +573,7 @@ def _differential_case(
|
|
|
543
573
|
|
|
544
574
|
|
|
545
575
|
_JIT_DIVERGENCES = _Failures()
|
|
576
|
+
_ENGINE_DIVERGENCES = _Failures()
|
|
546
577
|
|
|
547
578
|
|
|
548
579
|
def _differential_worker(worker_id: int, seed: int, deadline: float, failures: _Failures, ops: list[int]) -> None:
|
|
@@ -565,6 +596,15 @@ def test_clobber_differential_accuracy_threaded() -> None:
|
|
|
565
596
|
_run_workers(_differential_worker, _DIFF_DURATION, failures, seed, ops)
|
|
566
597
|
if failures.items():
|
|
567
598
|
pytest.fail("\n---\n".join(failures.items()))
|
|
599
|
+
engine_reports = _ENGINE_DIVERGENCES.items()
|
|
600
|
+
if engine_reports:
|
|
601
|
+
print(
|
|
602
|
+
f"[clobber-verify diff] {len(engine_reports)} accuracy divergence(s) "
|
|
603
|
+
f"on untrusted PCRE2 runtime {_pcre2_runtime_version()} (reported only):",
|
|
604
|
+
flush=True,
|
|
605
|
+
)
|
|
606
|
+
for report in engine_reports[:3]:
|
|
607
|
+
print(" " + report, flush=True)
|
|
568
608
|
jit_reports = _JIT_DIVERGENCES.items()
|
|
569
609
|
if jit_reports:
|
|
570
610
|
print(
|
|
@@ -358,10 +358,10 @@ def test_pcre2_replacement_conversion_tuple_format() -> None:
|
|
|
358
358
|
parsed_text = ([(1, 1)], ["$\\", None, "tail"])
|
|
359
359
|
parsed_bytes = ([(1, 1)], [b"$\\", None, b"tail"])
|
|
360
360
|
assert pcre_mod._pcre2_replacement_from_parsed(parsed_text, False) == (
|
|
361
|
-
"$$" + "\\" *
|
|
361
|
+
"$$" + "\\" * 2 + "${1}tail"
|
|
362
362
|
)
|
|
363
363
|
assert pcre_mod._pcre2_replacement_from_parsed(parsed_bytes, True) == (
|
|
364
|
-
b"$$" + b"\\" *
|
|
364
|
+
b"$$" + b"\\" * 2 + b"${1}tail"
|
|
365
365
|
)
|
|
366
366
|
|
|
367
367
|
|
|
@@ -46,8 +46,9 @@ def test_stdlib_parser_exported_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def test_flat_replacement_conversion_paths() -> None:
|
|
49
|
-
|
|
50
|
-
assert pcre_mod._pcre2_replacement_from_parsed([1, "$"],
|
|
49
|
+
# ${n} rather than \g<n>: PCRE2 only accepts \g<n> replacements from 10.44.
|
|
50
|
+
assert pcre_mod._pcre2_replacement_from_parsed([1, b"$"], True) == b"${1}$$"
|
|
51
|
+
assert pcre_mod._pcre2_replacement_from_parsed([1, "$"], False) == "${1}$$"
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
def test_replacement_template_cache_reuses_and_clears(
|
|
@@ -68,6 +68,7 @@ class TestThreadedBackend(unittest.TestCase):
|
|
|
68
68
|
self.assertTrue(pattern.use_threads)
|
|
69
69
|
|
|
70
70
|
def test_parallel_map_batches_work_without_changing_order(self):
|
|
71
|
+
self._skip_if_thread_barred()
|
|
71
72
|
pattern = pcre.compile(r"\w+", Flag.THREADS)
|
|
72
73
|
subjects = [word * 2_000 for word in ("alpha", "beta", "gamma", "delta", "epsilon")]
|
|
73
74
|
executor = thread_utils.ensure_thread_pool(2)
|
|
@@ -14,6 +14,12 @@ import pytest
|
|
|
14
14
|
import pcre.threads as threads_mod
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
_requires_thread_backend = pytest.mark.skipif(
|
|
18
|
+
not threads_mod.threading_supported(),
|
|
19
|
+
reason="threaded backend requires >=8 CPU cores",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
17
23
|
def test_threading_supported_false_on_low_core_count(
|
|
18
24
|
monkeypatch: pytest.MonkeyPatch,
|
|
19
25
|
) -> None:
|
|
@@ -51,6 +57,7 @@ def test_configure_threads_threshold_validation(
|
|
|
51
57
|
threads_mod.configure_threads(threshold=original)
|
|
52
58
|
|
|
53
59
|
|
|
60
|
+
@_requires_thread_backend
|
|
54
61
|
def test_configure_thread_pool_clamps_workers(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
55
62
|
original_workers = getattr(threads_mod, "_THREAD_POOL_WORKERS", None)
|
|
56
63
|
original_pool = getattr(threads_mod, "_THREAD_POOL", None)
|
|
@@ -71,6 +78,7 @@ def test_configure_thread_pool_clamps_workers(monkeypatch: pytest.MonkeyPatch) -
|
|
|
71
78
|
threads_mod._THREAD_POOL_WORKERS = original_workers
|
|
72
79
|
|
|
73
80
|
|
|
81
|
+
@_requires_thread_backend
|
|
74
82
|
def test_configure_thread_pool_rejects_invalid_worker_count() -> None:
|
|
75
83
|
with pytest.raises(TypeError):
|
|
76
84
|
threads_mod.configure_thread_pool(max_workers="x")
|
|
@@ -78,6 +86,7 @@ def test_configure_thread_pool_rejects_invalid_worker_count() -> None:
|
|
|
78
86
|
threads_mod.configure_thread_pool(max_workers=0)
|
|
79
87
|
|
|
80
88
|
|
|
89
|
+
@_requires_thread_backend
|
|
81
90
|
def test_get_thread_pool_size_initializes_once(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
82
91
|
original_workers = threads_mod._THREAD_POOL_WORKERS
|
|
83
92
|
original_pool = threads_mod._THREAD_POOL
|
|
@@ -159,6 +168,7 @@ def test_determine_worker_count_requires_supported_backend(
|
|
|
159
168
|
threads_mod._determine_worker_count(None)
|
|
160
169
|
|
|
161
170
|
|
|
171
|
+
@_requires_thread_backend
|
|
162
172
|
def test_configure_thread_pool_shuts_down_existing_pool() -> None:
|
|
163
173
|
original_pool = threads_mod._THREAD_POOL
|
|
164
174
|
original_workers = threads_mod._THREAD_POOL_WORKERS
|
|
@@ -176,6 +186,7 @@ def test_configure_thread_pool_shuts_down_existing_pool() -> None:
|
|
|
176
186
|
threads_mod._THREAD_POOL_WORKERS = original_workers
|
|
177
187
|
|
|
178
188
|
|
|
189
|
+
@_requires_thread_backend
|
|
179
190
|
def test_ensure_thread_pool_resizes_existing_pool() -> None:
|
|
180
191
|
original_pool = threads_mod._THREAD_POOL
|
|
181
192
|
original_workers = threads_mod._THREAD_POOL_WORKERS
|
|
@@ -192,6 +203,7 @@ def test_ensure_thread_pool_resizes_existing_pool() -> None:
|
|
|
192
203
|
threads_mod._THREAD_POOL_WORKERS = original_workers
|
|
193
204
|
|
|
194
205
|
|
|
206
|
+
@_requires_thread_backend
|
|
195
207
|
def test_pool_submission_cannot_race_executor_replacement(
|
|
196
208
|
monkeypatch: pytest.MonkeyPatch,
|
|
197
209
|
) -> None:
|
|
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
|