Skip to content

Commit 967d442

Browse files
authored
Merge pull request #4 from jg-rp/i-regexp
Map i-regexp pattern to PCRE-like pattern
2 parents 30d4400 + e107b6c commit 967d442

File tree

14 files changed

+203
-20
lines changed

14 files changed

+203
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
**Fixes**
66

77
- Handle end of query when lexing inside a filter expression.
8+
- Check patterns passed to `search` and `match` are valid I-Regexp patterns. Both of these functions now return _LogicalFalse_ if the pattern is not valid according to RFC 9485.
89

910
## Version 0.1.1
1011

jsonpath_rfc9535/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.1.1"
1+
__version__ = "0.1.2"

jsonpath_rfc9535/environment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def check_well_typedness(
217217
)
218218
elif typ == ExpressionType.LOGICAL:
219219
if not isinstance(
220-
arg, (FilterQuery, (LogicalExpression, ComparisonExpression))
220+
arg, (FilterQuery, LogicalExpression, ComparisonExpression)
221221
):
222222
raise JSONPathTypeError(
223223
f"{token.value}() argument {idx} must be of LogicalType",

jsonpath_rfc9535/filter_expressions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ def __eq__(self, other: object) -> bool:
186186

187187
def evaluate(self, context: FilterContext) -> bool:
188188
"""Evaluate the filter expression in the given _context_."""
189+
# TODO: sort circuit eval of right if left is false
189190
return _compare(
190191
self.left.evaluate(context), self.operator, self.right.evaluate(context)
191192
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from typing import List
2+
3+
4+
def map_re(pattern: str) -> str:
5+
escaped = False
6+
char_class = False
7+
parts: List[str] = []
8+
for ch in pattern:
9+
if escaped:
10+
parts.append(ch)
11+
escaped = False
12+
continue
13+
14+
if ch == ".":
15+
if not char_class:
16+
parts.append(r"(?:(?![\r\n])\P{Cs}|\p{Cs}\p{Cs})")
17+
else:
18+
parts.append(ch)
19+
elif ch == "\\":
20+
escaped = True
21+
parts.append(ch)
22+
elif ch == "[":
23+
char_class = True
24+
parts.append(ch)
25+
elif ch == "]":
26+
char_class = False
27+
parts.append(ch)
28+
else:
29+
parts.append(ch)
30+
31+
return "".join(parts)
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
"""The standard `match` function extension."""
22

33
import regex as re
4+
from iregexp_check import check
45

56
from jsonpath_rfc9535.function_extensions import ExpressionType
67
from jsonpath_rfc9535.function_extensions import FilterFunction
78

9+
from ._pattern import map_re
10+
811

912
class Match(FilterFunction):
1013
"""The standard `match` function."""
1114

1215
arg_types = [ExpressionType.VALUE, ExpressionType.VALUE]
1316
return_type = ExpressionType.LOGICAL
1417

15-
def __call__(self, string: str, pattern: str) -> bool:
18+
def __call__(self, string: str, pattern: object) -> bool:
1619
"""Return `True` if _string_ matches _pattern_, or `False` otherwise."""
20+
if not isinstance(pattern, str) or not check(pattern):
21+
return False
22+
1723
try:
1824
# re.fullmatch caches compiled patterns internally
19-
return bool(re.fullmatch(pattern, string))
25+
return bool(re.fullmatch(map_re(pattern), string))
2026
except (TypeError, re.error):
2127
return False
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
"""The standard `search` function extension."""
22

33
import regex as re
4+
from iregexp_check import check
45

56
from jsonpath_rfc9535.function_extensions import ExpressionType
67
from jsonpath_rfc9535.function_extensions import FilterFunction
78

9+
from ._pattern import map_re
10+
811

912
class Search(FilterFunction):
1013
"""The standard `search` function."""
1114

1215
arg_types = [ExpressionType.VALUE, ExpressionType.VALUE]
1316
return_type = ExpressionType.LOGICAL
1417

15-
def __call__(self, string: str, pattern: str) -> bool:
18+
def __call__(self, string: str, pattern: object) -> bool:
1619
"""Return `True` if _string_ contains _pattern_, or `False` otherwise."""
20+
if not isinstance(pattern, str) or not check(pattern):
21+
return False
22+
1723
try:
1824
# re.search caches compiled patterns internally
19-
return bool(re.search(pattern, string))
25+
return bool(re.search(map_re(pattern), string, re.VERSION1))
2026
except (TypeError, re.error):
2127
return False

jsonpath_rfc9535/parse.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ def __init__(self, *, env: JSONPathEnvironment) -> None:
114114
TokenType.TRUE: self.parse_boolean,
115115
}
116116

117+
# TODO: can a function argument be a grouped expression?
118+
# TODO: can a function argument contain a !?
119+
117120
self.function_argument_map: Dict[
118121
TokenType, Callable[[TokenStream], Expression]
119122
] = {
@@ -412,6 +415,7 @@ def parse_grouped_expression(self, stream: TokenStream) -> Expression:
412415
raise JSONPathSyntaxError(
413416
"unbalanced parentheses", token=stream.current
414417
)
418+
# TODO: only if binary op
415419
expr = self.parse_infix_expression(stream, expr)
416420

417421
stream.expect(TokenType.RPAREN)

jsonpath_rfc9535/query.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class JSONPathQuery:
3232
segments: The `JSONPathSegment` instances that make up this query.
3333
"""
3434

35-
__slots__ = ("env", "fake_root", "segments")
35+
__slots__ = ("env", "segments")
3636

3737
def __init__(
3838
self,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ classifiers = [
2424
"Programming Language :: Python :: Implementation :: CPython",
2525
"Programming Language :: Python :: Implementation :: PyPy",
2626
]
27-
dependencies = ["regex"]
27+
dependencies = ["regex", "iregexp-check>=0.1.3"]
2828

2929
[project.urls]
3030
Documentation = "https://jg-rp.github.io/python-jsonpath-rfc9535/"

0 commit comments

Comments
 (0)