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.
Files changed (80) hide show
  1. {pypcre-0.6.1 → pypcre-0.6.2}/PKG-INFO +1 -1
  2. {pypcre-0.6.1 → pypcre-0.6.2}/PyPcre.egg-info/PKG-INFO +1 -1
  3. {pypcre-0.6.1 → pypcre-0.6.2}/pcre/pcre.py +10 -5
  4. {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/pcre2.c +80 -16
  5. {pypcre-0.6.1 → pypcre-0.6.2}/pyproject.toml +1 -1
  6. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_clobber_verify.py +43 -3
  7. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_coverage_gaps.py +2 -2
  8. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_python_coverage_audit.py +3 -2
  9. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_threaded_backend.py +1 -0
  10. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_threads.py +12 -0
  11. {pypcre-0.6.1 → pypcre-0.6.2}/LICENSE +0 -0
  12. {pypcre-0.6.1 → pypcre-0.6.2}/MANIFEST.in +0 -0
  13. {pypcre-0.6.1 → pypcre-0.6.2}/PyPcre.egg-info/SOURCES.txt +0 -0
  14. {pypcre-0.6.1 → pypcre-0.6.2}/PyPcre.egg-info/dependency_links.txt +0 -0
  15. {pypcre-0.6.1 → pypcre-0.6.2}/PyPcre.egg-info/top_level.txt +0 -0
  16. {pypcre-0.6.1 → pypcre-0.6.2}/README.md +0 -0
  17. {pypcre-0.6.1 → pypcre-0.6.2}/pcre/__init__.py +0 -0
  18. {pypcre-0.6.1 → pypcre-0.6.2}/pcre/_stdlib_re.py +0 -0
  19. {pypcre-0.6.1 → pypcre-0.6.2}/pcre/cache.py +0 -0
  20. {pypcre-0.6.1 → pypcre-0.6.2}/pcre/error.py +0 -0
  21. {pypcre-0.6.1 → pypcre-0.6.2}/pcre/flags.py +0 -0
  22. {pypcre-0.6.1 → pypcre-0.6.2}/pcre/re_compat.py +0 -0
  23. {pypcre-0.6.1 → pypcre-0.6.2}/pcre/threads.py +0 -0
  24. {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/atomic_compat.h +0 -0
  25. {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/cache.c +0 -0
  26. {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/error.c +0 -0
  27. {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/flag.c +0 -0
  28. {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/jit_support.c +0 -0
  29. {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/memory.c +0 -0
  30. {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/pattern_cache.c +0 -0
  31. {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/pcre2.h +0 -0
  32. {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/pcre2_module.h +0 -0
  33. {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/string_helpers.c +0 -0
  34. {pypcre-0.6.1 → pypcre-0.6.2}/pcre_ext/util.c +0 -0
  35. {pypcre-0.6.1 → pypcre-0.6.2}/setup.cfg +0 -0
  36. {pypcre-0.6.1 → pypcre-0.6.2}/setup.py +0 -0
  37. {pypcre-0.6.1 → pypcre-0.6.2}/setup_utils.py +0 -0
  38. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_accuracy.py +0 -0
  39. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_api_parity.py +0 -0
  40. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_basic.py +0 -0
  41. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_bench_string.py +0 -0
  42. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_benchmark.py +0 -0
  43. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_bytes.py +0 -0
  44. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_c_api_audit.py +0 -0
  45. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_cache.py +0 -0
  46. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_cache_global.py +0 -0
  47. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_cache_global_inproc.py +0 -0
  48. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_cache_scope_safety.py +0 -0
  49. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_clobber.py +0 -0
  50. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_clobber_thread.py +0 -0
  51. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_compile_utf_no_check_safety.py +0 -0
  52. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_core.py +0 -0
  53. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_errors.py +0 -0
  54. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_escape_fastpath.py +0 -0
  55. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_expand_fastpath.py +0 -0
  56. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_flags.py +0 -0
  57. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_gil_zero_safety.py +0 -0
  58. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_jit.py +0 -0
  59. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_jit_anchoring.py +0 -0
  60. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_jit_start_optimize_workaround.py +0 -0
  61. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_lastindex_ovector_fastpath.py +0 -0
  62. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_lastindex_semantics.py +0 -0
  63. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_literal_capture_findall_fastpath.py +0 -0
  64. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_literal_capture_split_fastpath.py +0 -0
  65. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_memory.py +0 -0
  66. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_mmap.py +0 -0
  67. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_module.py +0 -0
  68. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_multi_literal_capture_findall_fastpath.py +0 -0
  69. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_multi_literal_capture_split_fastpath.py +0 -0
  70. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_pattern.py +0 -0
  71. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_re_compat.py +0 -0
  72. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_setup_utils_multiarch.py +0 -0
  73. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_simd.py +0 -0
  74. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_stdlib_flag_fastpath.py +0 -0
  75. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_sub_count_one_fastpath.py +0 -0
  76. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_sub_reference_fastpath.py +0 -0
  77. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_template_dispatch_fastpath.py +0 -0
  78. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_transformers_regex.py +0 -0
  79. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_utf8.py +0 -0
  80. {pypcre-0.6.1 → pypcre-0.6.2}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPcre
3
- Version: 0.6.1
3
+ Version: 0.6.2
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPcre
3
- Version: 0.6.1
3
+ Version: 0.6.2
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
@@ -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(("\\g<" + str(slot_to_group[i]) + ">").encode("ascii"))
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("\\g<" + str(slot_to_group[i]) + ">")
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(("\\g<" + str(item) + ">").encode("ascii"))
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("\\g<" + str(item) + ">")
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
- if (length > PY_SSIZE_T_MAX - 3) {
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 + 3;
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] = 'g';
5207
- output[slash_index + 2] = '<';
5208
- output[slash_index + 3] = (char)following;
5209
- output[slash_index + 4] = '>';
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, '\\') < 0 ||
5227
- PyUnicode_WriteChar(result, slash_index + 1, 'g') < 0 ||
5228
- PyUnicode_WriteChar(result, slash_index + 2, '<') < 0 ||
5229
- PyUnicode_WriteChar(result, slash_index + 3, following) < 0 ||
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 + 5,
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
- Py_INCREF(replacement);
5291
- return replacement;
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.1"
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
- quantify = quantify and not body_nullable
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
- failures.record(
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
- "$$" + "\\" * 3 + "g<1>tail"
361
+ "$$" + "\\" * 2 + "${1}tail"
362
362
  )
363
363
  assert pcre_mod._pcre2_replacement_from_parsed(parsed_bytes, True) == (
364
- b"$$" + b"\\" * 3 + b"g<1>tail"
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
- assert pcre_mod._pcre2_replacement_from_parsed([1, b"$"], True) == b"\\g<1>$$"
50
- assert pcre_mod._pcre2_replacement_from_parsed([1, "$"], False) == r"\g<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