diff --git a/README.md b/README.md index 235bbb7..e5ff9aa 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/pcre/__init__.py b/pcre/__init__.py index 95f6d1d..f94b683 100644 --- a/pcre/__init__.py +++ b/pcre/__init__.py @@ -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(): diff --git a/pcre/_stdlib_re.py b/pcre/_stdlib_re.py new file mode 100644 index 0000000..a431683 --- /dev/null +++ b/pcre/_stdlib_re.py @@ -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"] diff --git a/pcre/pcre.py b/pcre/pcre.py index e539c5e..06fa1be 100644 --- a/pcre/pcre.py +++ b/pcre/pcre.py @@ -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 @@ -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( @@ -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"}) diff --git a/pcre/re_compat.py b/pcre/re_compat.py index 8fefcda..c36653b 100644 --- a/pcre/re_compat.py +++ b/pcre/re_compat.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 9bf0001..fce54e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/tests/test_api_parity.py b/tests/test_api_parity.py index e8d21be..061f38c 100644 --- a/tests/test_api_parity.py +++ b/tests/test_api_parity.py @@ -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(): @@ -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) diff --git a/tests/test_core.py b/tests/test_core.py index b4c5264..55f5bf4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,5 +1,6 @@ import types from collections import OrderedDict +import re import pytest from pcre import Flag @@ -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(