Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add benchmark and profile scripts. #43

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
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