Skip to content

Commit

Permalink
Add benchmark and profile scripts.
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed Oct 17, 2023
1 parent 39ee4cf commit b380ebd
Show file tree
Hide file tree
Showing 10 changed files with 318 additions and 155 deletions.
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ ENV/

# Dev utils
dev.py
benchmark.py
profile_.py

# Test fixtures
comparison_regression_suite.yaml
Expand Down
2 changes: 1 addition & 1 deletion jsonpath/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@
from .parse import Parser
from .path import CompoundJSONPath
from .path import JSONPath
from .stream import TokenStream
from .token import TOKEN_EOF
from .token import TOKEN_INTERSECTION
from .token import TOKEN_UNION
from .token import Token
from .token import TokenStream

if TYPE_CHECKING:
from io import IOBase
Expand Down
16 changes: 13 additions & 3 deletions jsonpath/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ class JSONPathMatch:
"obj",
"parent",
"parts",
"path",
"root",
)

Expand All @@ -49,7 +48,6 @@ def __init__(
filter_context: FilterContextVars,
obj: object,
parent: Optional[JSONPathMatch],
path: str,
parts: Tuple[PathPart, ...],
root: Union[Sequence[Any], Mapping[str, Any]],
) -> None:
Expand All @@ -58,12 +56,16 @@ def __init__(
self.obj: object = obj
self.parent: Optional[JSONPathMatch] = parent
self.parts: Tuple[PathPart, ...] = parts
self.path: str = path
self.root: Union[Sequence[Any], Mapping[str, Any]] = root

def __str__(self) -> str:
return f"{_truncate(str(self.obj), 5)!r} @ {_truncate(self.path, 5)}"

@property
def path(self) -> str:
"""The canonical string representation of the path to this match."""
return "$" + "".join((_path_repr(p) for p in self.parts))

def add_child(self, *children: JSONPathMatch) -> None:
"""Append one or more children to this match."""
self.children.extend(children)
Expand All @@ -86,6 +88,14 @@ def _truncate(val: str, num: int, end: str = "...") -> str:
return " ".join(words[:num]) + end


def _path_repr(part: Union[str, int]) -> str:
if isinstance(part, str):
if len(part) > 1 and part.startswith(("#", "~")):
return f"[{part}]"
return f"['{part}']"
return f"[{part}]"


class NodeList(List[JSONPathMatch]):
"""List of JSONPathMatch objects, analogous to the spec's nodelist."""

Expand Down
63 changes: 35 additions & 28 deletions jsonpath/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@

if TYPE_CHECKING:
from .env import JSONPathEnvironment
from .stream import TokenStream
from .token import TokenStream

# ruff: noqa: D102

Expand Down Expand Up @@ -197,6 +197,13 @@ class Parser:
]
)

END_SELECTOR = frozenset(
[
TOKEN_EOF,
TOKEN_RBRACKET,
]
)

RE_FLAG_MAP = {
"a": re.A,
"i": re.I,
Expand Down Expand Up @@ -269,7 +276,7 @@ def __init__(self, *, env: JSONPathEnvironment) -> None:
def parse(self, stream: TokenStream) -> Iterable[JSONPathSelector]:
"""Parse a JSONPath from a stream of tokens."""
if stream.current.kind == TOKEN_ROOT:
stream.next_token()
next(stream)
yield from self.parse_path(stream, in_filter=False)

if stream.current.kind not in (TOKEN_EOF, TOKEN_INTERSECTION, TOKEN_UNION):
Expand Down Expand Up @@ -316,16 +323,16 @@ def parse_path(
yield self.parse_selector_list(stream)
else:
if in_filter:
stream.push(stream.current)
stream.backup()
break

stream.next_token()
next(stream)

def parse_slice(self, stream: TokenStream) -> SliceSelector:
"""Parse a slice JSONPath expression from a stream of tokens."""
start_token = stream.next_token()
start_token = next(stream)
stream.expect(TOKEN_SLICE_STOP)
stop_token = stream.next_token()
stop_token = next(stream)
stream.expect(TOKEN_SLICE_STEP)
step_token = stream.current

Expand Down Expand Up @@ -354,7 +361,7 @@ def parse_slice(self, stream: TokenStream) -> SliceSelector:

def parse_selector_list(self, stream: TokenStream) -> ListSelector: # noqa: PLR0912
"""Parse a comma separated list JSONPath selectors from a stream of tokens."""
tok = stream.next_token()
tok = next(stream)
list_items: List[
Union[
IndexSelector,
Expand Down Expand Up @@ -448,17 +455,17 @@ def parse_selector_list(self, stream: TokenStream) -> ListSelector: # noqa: PLR
if stream.peek.kind != TOKEN_RBRACKET:
# TODO: error message .. expected a comma or logical operator
stream.expect_peek(TOKEN_COMMA)
stream.next_token()
next(stream)

stream.next_token()
next(stream)

if not list_items:
raise JSONPathSyntaxError("empty bracketed segment", token=tok)

return ListSelector(env=self.env, token=tok, items=list_items)

def parse_filter(self, stream: TokenStream) -> Filter:
tok = stream.next_token()
tok = next(stream)
expr = self.parse_filter_selector(stream)

if self.env.well_typed and isinstance(expr, FunctionExtension):
Expand Down Expand Up @@ -496,7 +503,7 @@ def parse_float_literal(self, stream: TokenStream) -> FilterExpression:
return FloatLiteral(value=float(stream.current.value))

def parse_prefix_expression(self, stream: TokenStream) -> FilterExpression:
tok = stream.next_token()
tok = next(stream)
assert tok.kind == TOKEN_NOT
return PrefixExpression(
operator="!",
Expand All @@ -506,7 +513,7 @@ def parse_prefix_expression(self, stream: TokenStream) -> FilterExpression:
def parse_infix_expression(
self, stream: TokenStream, left: FilterExpression
) -> FilterExpression:
tok = stream.next_token()
tok = next(stream)
precedence = self.PRECEDENCES.get(tok.kind, self.PRECEDENCE_LOWEST)
right = self.parse_filter_selector(stream, precedence)
operator = self.BINARY_OPERATORS[tok.kind]
Expand All @@ -521,9 +528,9 @@ def parse_infix_expression(
return InfixExpression(left, operator, right)

def parse_grouped_expression(self, stream: TokenStream) -> FilterExpression:
stream.next_token()
next(stream)
expr = self.parse_filter_selector(stream)
stream.next_token()
next(stream)

while stream.current.kind != TOKEN_RPAREN:
if stream.current.kind == TOKEN_EOF:
Expand All @@ -536,13 +543,13 @@ def parse_grouped_expression(self, stream: TokenStream) -> FilterExpression:
return expr

def parse_root_path(self, stream: TokenStream) -> FilterExpression:
stream.next_token()
next(stream)
return RootPath(
JSONPath(env=self.env, selectors=self.parse_path(stream, in_filter=True))
)

def parse_self_path(self, stream: TokenStream) -> FilterExpression:
stream.next_token()
next(stream)
return SelfPath(
JSONPath(env=self.env, selectors=self.parse_path(stream, in_filter=True))
)
Expand All @@ -551,22 +558,22 @@ def parse_current_key(self, _: TokenStream) -> FilterExpression:
return CURRENT_KEY

def parse_filter_context_path(self, stream: TokenStream) -> FilterExpression:
stream.next_token()
next(stream)
return FilterContextPath(
JSONPath(env=self.env, selectors=self.parse_path(stream, in_filter=True))
)

def parse_regex(self, stream: TokenStream) -> FilterExpression:
pattern = stream.current.value
if stream.peek.kind == TOKEN_RE_FLAGS:
stream.next_token()
next(stream)
flags = 0
for flag in set(stream.current.value):
flags |= self.RE_FLAG_MAP[flag]
return RegexLiteral(value=re.compile(pattern, flags))

def parse_list_literal(self, stream: TokenStream) -> FilterExpression:
stream.next_token()
next(stream)
list_items: List[FilterExpression] = []

while stream.current.kind != TOKEN_RBRACKET:
Expand All @@ -580,15 +587,15 @@ def parse_list_literal(self, stream: TokenStream) -> FilterExpression:

if stream.peek.kind != TOKEN_RBRACKET:
stream.expect_peek(TOKEN_COMMA)
stream.next_token()
next(stream)

stream.next_token()
next(stream)

return ListLiteral(list_items)

def parse_function_extension(self, stream: TokenStream) -> FilterExpression:
function_arguments: List[FilterExpression] = []
tok = stream.next_token()
tok = next(stream)

while stream.current.kind != TOKEN_RPAREN:
try:
Expand All @@ -604,17 +611,17 @@ def parse_function_extension(self, stream: TokenStream) -> FilterExpression:
# The argument could be a comparison or logical expression
peek_kind = stream.peek.kind
while peek_kind in self.BINARY_OPERATORS:
stream.next_token()
next(stream)
expr = self.parse_infix_expression(stream, expr)
peek_kind = stream.peek.kind

function_arguments.append(expr)

if stream.peek.kind != TOKEN_RPAREN:
stream.expect_peek(TOKEN_COMMA)
stream.next_token()
next(stream)

stream.next_token()
next(stream)

return FunctionExtension(
tok.value,
Expand All @@ -627,7 +634,7 @@ def parse_filter_selector(
try:
left = self.token_map[stream.current.kind](stream)
except KeyError as err:
if stream.current.kind in (TOKEN_EOF, TOKEN_RBRACKET):
if stream.current.kind in self.END_SELECTOR:
msg = "end of expression"
else:
msg = repr(stream.current.value)
Expand All @@ -638,15 +645,15 @@ def parse_filter_selector(
while True:
peek_kind = stream.peek.kind
if (
peek_kind in (TOKEN_EOF, TOKEN_RBRACKET)
peek_kind in self.END_SELECTOR
or self.PRECEDENCES.get(peek_kind, self.PRECEDENCE_LOWEST) < precedence
):
break

if peek_kind not in self.BINARY_OPERATORS:
return left

stream.next_token()
next(stream)
left = self.parse_infix_expression(stream, left)

return left
Expand Down
2 changes: 0 additions & 2 deletions jsonpath/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ def finditer(
filter_context=filter_context or {},
obj=_data,
parent=None,
path=self.env.root_token,
parts=(),
root=_data,
)
Expand Down Expand Up @@ -163,7 +162,6 @@ async def root_iter() -> AsyncIterable[JSONPathMatch]:
filter_context=filter_context or {},
obj=_data,
parent=None,
path=self.env.root_token,
parts=(),
root=_data,
)
Expand Down
Loading

0 comments on commit b380ebd

Please sign in to comment.