Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Modern `nogil` Python bindings for the Pcre2 library with `stdlib.re` api compat


## Latest News
* 03/21/2026 [0.2.14](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.4): Python 3.14 compat
* 03/02/2026 [0.2.11](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.11): Auto-detect `Visual Studio` for `Windows` env during install/compile.
* 02/24/2026 [0.2.10](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.10): Allow VisualStudio (VS) compiler version check override via env var.
* 12/15/2025 [0.2.8](https://github.com/ModelCloud/PyPcre/releases/tag/v0.2.8): Fixed multi-arch Linux os compatibility where both x86_64 and i386 libs of pcre2 are installed.
Expand Down
2 changes: 2 additions & 0 deletions pcre/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ def escape(pattern: Any) -> Any:
"A": Flag.NO_UTF | Flag.NO_UCP,
"UNICODE": _FLAG_ZERO,
"U": _FLAG_ZERO,
"TEMPLATE": _FLAG_ZERO,
"T": _FLAG_ZERO,
}

for _alias, _flag in _FLAG_COMPAT_ALIASES.items():
Expand Down
28 changes: 28 additions & 0 deletions pcre/_stdlib_re.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Compatibility helpers for private stdlib :mod:`re` internals."""

from __future__ import annotations

import re as _std_re
import warnings


def _load_parser():
parser = getattr(_std_re, "_parser", None)
if parser is not None:
return parser

with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
import sre_parse as parser

return parser


_parser = _load_parser()

RE_TEMPLATE = getattr(_std_re, "TEMPLATE", 0)
RE_TEMPLATE_FLAG: int = int(getattr(_std_re, "TEMPLATE", 0))
RE_UNICODE_FLAG: int = int(getattr(_std_re, "UNICODE", 0))


__all__ = ["_parser", "RE_TEMPLATE", "RE_TEMPLATE_FLAG", "RE_UNICODE_FLAG"]
9 changes: 3 additions & 6 deletions pcre/pcre.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,11 @@

import re as _std_re
from collections.abc import Generator, Iterable
try:
from re import _parser, TEMPLATE # python 3.11+
except Exception:
import sre_parse as _parser
from typing import Any, List

import pcre_ext_c as _pcre2

from ._stdlib_re import RE_TEMPLATE, RE_TEMPLATE_FLAG, RE_UNICODE_FLAG, _parser
from .cache import cached_compile
from .cache import clear_cache as _clear_cache
from .flags import Flag, strip_py_only_flags
Expand Down Expand Up @@ -128,7 +125,7 @@ def _apply_default_unicode_flags(pattern: Any, flags: int) -> int:


def _coerce_stdlib_regexflag(flag: _std_re.RegexFlag) -> int:
unsupported_bits = int(flag) & ~_STD_RE_FLAG_MASK
unsupported_bits = int(flag) & ~(_STD_RE_FLAG_MASK | RE_TEMPLATE_FLAG | RE_UNICODE_FLAG)
if unsupported_bits:
unsupported = _std_re.RegexFlag(unsupported_bits)
raise ValueError(
Expand Down Expand Up @@ -618,7 +615,7 @@ def template(pattern, flags=0):
"without an obvious purpose. "
"Use re.compile() instead.",
DeprecationWarning)
return compile(pattern, flags | TEMPLATE)
return compile(pattern, flags | RE_TEMPLATE)

_PARALLEL_EXEC_METHODS = frozenset({"match", "search", "fullmatch", "findall"})

Expand Down
6 changes: 2 additions & 4 deletions pcre/re_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@

import operator
import re as _std_re
try:
from re import _parser # python 3.11+
except Exception:
import sre_parse as _parser
from typing import Any, List

import pcre_ext_c as _pcre2

from ._stdlib_re import _parser


_CRawMatch = _pcre2.Match

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "PyPcre"
version = "0.2.13"
version = "0.2.14"
description = "Modern, GIL-friendly, Fast Python bindings for PCRE2 with auto caching and JIT of compiled patterns."
readme = "README.md"
requires-python = ">=3.9"
Expand Down
9 changes: 9 additions & 0 deletions tests/test_api_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def test_stdlib_style_flag_aliases():
assert pcre.A == ascii_alias
assert pcre.UNICODE == 0
assert pcre.U == 0
assert pcre.TEMPLATE == 0
assert pcre.T == 0


def test_specific_compile_error_exposes_dedicated_exception():
Expand Down Expand Up @@ -83,6 +85,13 @@ def test_compile_accepts_stdlib_regex_flags():
assert combo.flags & Flag.MULTILINE


def test_compile_accepts_noop_stdlib_regex_flags():
compiled = pcre.compile(r"pattern", flags=re.RegexFlag.UNICODE)
assert isinstance(compiled, pcre.Pattern)
assert compiled.flags & Flag.UTF
assert compiled.flags & Flag.UCP


def test_compile_rejects_incompatible_stdlib_regex_flags():
with pytest.raises(ValueError):
pcre.compile(r"pattern", flags=re.RegexFlag.DEBUG)
Expand Down
30 changes: 30 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import types
from collections import OrderedDict
import re

import pytest
from pcre import Flag
Expand Down Expand Up @@ -288,6 +289,35 @@ def test_compile_rejects_non_int_iterable_flags():
core.compile("expr", flags=("not", "ints"))


def test_template_delegates_to_compile_without_template_bits(monkeypatch):
captured = {}

def fake_cached(pattern, flags, wrapper, *, jit):
captured["flags"] = flags
fake_cpattern = types.SimpleNamespace(
pattern=pattern,
groupindex={},
flags=flags,
match=MethodRecorder("match"),
search=MethodRecorder("search"),
fullmatch=MethodRecorder("fullmatch"),
jit=jit,
)
return wrapper(fake_cpattern)

monkeypatch.setattr(core, "cached_compile", fake_cached)
monkeypatch.setattr(core, "RE_TEMPLATE", re.RegexFlag.UNICODE)

with pytest.warns(DeprecationWarning):
compiled = core.template("expr")

expected_flags = strip_py_only_flags(
core._apply_default_unicode_flags("expr", 0)
)
assert captured["flags"] == expected_flags
assert compiled.flags == expected_flags


def test_pattern_match_handles_optional_end():
match_method = MethodRecorder(FakeMatch((0, 3), group0="matched"))
fake_cpattern = types.SimpleNamespace(
Expand Down
Loading