Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ format: install $(BIN)/license-header ## Format code
.PHONY: test
test: generate install gettestdata ## Run unit tests
uv run -- python -m unittest
$(MAKE) testextra

.PHONY: testextra
testextra:
uv pip install .[re2]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to avoid uv pip, and instead do something like uv sync --extra re2?: https://docs.astral.sh/uv/concepts/projects/sync/#syncing-optional-dependencies

uv run -- python -m unittest

.PHONY: conformance
conformance: $(BIN)/protovalidate-conformance generate install ## Run conformance tests
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ To install the package, use `pip`:
pip install protovalidate
```

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:

```shell
pip install protovalidate[re2]
```

## Documentation

Comprehensive documentation for Protovalidate is available in [Buf's documentation library][protovalidate].
Expand Down
29 changes: 29 additions & 0 deletions protovalidate/internal/extra_func.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
from protovalidate.internal import string_format
from protovalidate.internal.rules import MessageType, field_to_cel

_USE_RE2 = True
try:
import re2 # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one other minor thing, if we want: could install the google-re2-stubs package as a dev dependency, which would give us type info for re2 (and we could nix the ignore here).

ref: https://github.com/ddn0/google-re2-stubs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(published by the same person who had asked for re2 compatibility, here: #145)

except ImportError:
_USE_RE2 = False

# See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
_email_regex = re.compile(
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])?)*$"
Expand Down Expand Up @@ -1553,11 +1559,34 @@ def __peek(self, char: str) -> bool:
return self._index < len(self._string) and self._string[self._index] == char


def cel_matches_re(text: str, pattern: str) -> celpy.Result:
try:
m = re.search(pattern, text)
except re.error as ex:
return celpy.CELEvalError("match error", ex.__class__, ex.args)

return celtypes.BoolType(m is not None)


def cel_matches_re2(text: str, pattern: str) -> celpy.Result:
try:
m = re2.search(pattern, text)
except re2.error as ex:
return celpy.CELEvalError("match error", ex.__class__, ex.args)

return celtypes.BoolType(m is not None)


cel_matches = cel_matches_re2 if _USE_RE2 else cel_matches_re


def make_extra_funcs() -> dict[str, celpy.CELFunction]:
string_fmt = string_format.StringFormat()
return {
# Missing standard functions
"format": string_fmt.format,
# Overridden standard functions
"matches": cel_matches,
# protovalidate specific functions
"getField": cel_get_field,
"isNan": cel_is_nan,
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ dependencies = [
"protobuf==6.*",
"cel-python==0.2.*",
]
[project.optional-dependencies]
re2 = [
"google-re2==1.*",
]

[project.urls]
Homepage = "https://github.com/bufbuild/protovalidate-python"
Expand Down
44 changes: 44 additions & 0 deletions test/test_matches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2023-2025 Buf Technologies, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import importlib.util
import unittest

import celpy
from celpy import celtypes

from protovalidate.internal.extra_func import cel_matches_re, cel_matches_re2

_USE_RE2 = True
spec = importlib.util.find_spec("re2")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if spec is None:
_USE_RE2 = False


class TestCollectViolations(unittest.TestCase):
@unittest.skipUnless(_USE_RE2, "Requires 're2'")
def test_function_matches_re2(self):
empty_string = celtypes.StringType("")
# \z is valid re2 syntax for end of text
self.assertTrue(cel_matches_re2(empty_string, "^\\z"))
# \Z is invalid re2 syntax
self.assertIsInstance(cel_matches_re2(empty_string, "^\\Z"), celpy.CELEvalError)

@unittest.skipUnless(_USE_RE2 is False, "Requires 're'")
def test_function_matches_re(self):
empty_string = celtypes.StringType("")
# \z is invalid re syntax
self.assertIsInstance(cel_matches_re(empty_string, "^\\z"), celpy.CELEvalError)
# \Z is valid re syntax for end of text
self.assertTrue(cel_matches_re(empty_string, "^\\Z"))
Loading