Skip to content

Commit eb3c727

Browse files
authored
Add re2 as optional dependency (#346)
This adds `re2` as an optional dependency to protovalidate-python. With this configured, users can choose to optionally install re2 if they explicit re2 functionality. For example `pip install protovalidate[re2]` Note that while CEL requires re2 as part of the spec, our current version of cel-python (v0.2.0) doesn't. We are unable to upgrade cel-python beyond v0.2.0 at this point due to some outstanding issues, so since re2 now supports Python 3.13, we're adding it as an optional dep directly to protovalidate-python rather than the re2 workaround we had previously implemented.
1 parent ef083bc commit eb3c727

File tree

6 files changed

+222
-0
lines changed

6 files changed

+222
-0
lines changed

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ format: install $(BIN)/license-header ## Format code
5252
.PHONY: test
5353
test: generate install gettestdata ## Run unit tests
5454
uv run -- python -m unittest
55+
$(MAKE) testextra
56+
57+
.PHONY: testextra
58+
testextra:
59+
uv sync --extra re2
60+
uv run -- python -m unittest
5561

5662
.PHONY: conformance
5763
conformance: $(BIN)/protovalidate-conformance generate install ## Run conformance tests

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ To install the package, use `pip`:
5252
pip install protovalidate
5353
```
5454

55+
Protovalidate also has an optional dependency on [google-re2](https://pypi.org/project/google-re2/). If you require re2 syntax for your regular expressions, install the extra dep as follows:
56+
57+
```shell
58+
pip install protovalidate[re2]
59+
```
60+
5561
## Documentation
5662

5763
Comprehensive documentation for Protovalidate is available in [Buf's documentation library][protovalidate].

protovalidate/internal/extra_func.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
from protovalidate.internal import string_format
2424
from protovalidate.internal.rules import MessageType, field_to_cel
2525

26+
_USE_RE2 = True
27+
try:
28+
import re2
29+
except ImportError:
30+
_USE_RE2 = False
31+
2632
# See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
2733
_email_regex = re.compile(
2834
r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
@@ -1553,11 +1559,34 @@ def __peek(self, char: str) -> bool:
15531559
return self._index < len(self._string) and self._string[self._index] == char
15541560

15551561

1562+
def cel_matches_re(text: str, pattern: str) -> celpy.Result:
1563+
try:
1564+
m = re.search(pattern, text)
1565+
except re.error as ex:
1566+
return celpy.CELEvalError("match error", ex.__class__, ex.args)
1567+
1568+
return celtypes.BoolType(m is not None)
1569+
1570+
1571+
def cel_matches_re2(text: str, pattern: str) -> celpy.Result:
1572+
try:
1573+
m = re2.search(pattern, text)
1574+
except re2.error as ex:
1575+
return celpy.CELEvalError("match error", ex.__class__, ex.args)
1576+
1577+
return celtypes.BoolType(m is not None)
1578+
1579+
1580+
cel_matches = cel_matches_re2 if _USE_RE2 else cel_matches_re
1581+
1582+
15561583
def make_extra_funcs() -> dict[str, celpy.CELFunction]:
15571584
string_fmt = string_format.StringFormat()
15581585
return {
15591586
# Missing standard functions
15601587
"format": string_fmt.format,
1588+
# Overridden standard functions
1589+
"matches": cel_matches,
15611590
# protovalidate specific functions
15621591
"getField": cel_get_field,
15631592
"isNan": cel_is_nan,

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ dependencies = [
2424
"protobuf==6.*",
2525
"cel-python==0.2.*",
2626
]
27+
[project.optional-dependencies]
28+
re2 = [
29+
"google-re2==1.*",
30+
]
2731

2832
[project.urls]
2933
Homepage = "https://github.com/bufbuild/protovalidate-python"
@@ -32,6 +36,7 @@ Issues = "https://github.com/bufbuild/protovalidate-python/issues"
3236

3337
[dependency-groups]
3438
dev = [
39+
"google-re2-stubs>=0.1.1",
3540
"mypy",
3641
"ruff",
3742
"types-protobuf==6.30.2.20250503",

test/test_matches.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2023-2025 Buf Technologies, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import importlib.util
16+
import unittest
17+
18+
import celpy
19+
from celpy import celtypes
20+
21+
from protovalidate.internal.extra_func import cel_matches_re, cel_matches_re2
22+
23+
_USE_RE2 = True
24+
spec = importlib.util.find_spec("re2")
25+
if spec is None:
26+
_USE_RE2 = False
27+
28+
29+
class TestCollectViolations(unittest.TestCase):
30+
@unittest.skipUnless(_USE_RE2, "Requires 're2'")
31+
def test_function_matches_re2(self):
32+
empty_string = celtypes.StringType("")
33+
# \z is valid re2 syntax for end of text
34+
self.assertTrue(cel_matches_re2(empty_string, "^\\z"))
35+
# \Z is invalid re2 syntax
36+
self.assertIsInstance(cel_matches_re2(empty_string, "^\\Z"), celpy.CELEvalError)
37+
38+
@unittest.skipUnless(_USE_RE2 is False, "Requires 're'")
39+
def test_function_matches_re(self):
40+
empty_string = celtypes.StringType("")
41+
# \z is invalid re syntax
42+
self.assertIsInstance(cel_matches_re(empty_string, "^\\z"), celpy.CELEvalError)
43+
# \Z is valid re syntax for end of text
44+
self.assertTrue(cel_matches_re(empty_string, "^\\Z"))

0 commit comments

Comments
 (0)