Skip to content

Commit

Permalink
Merge pull request #8 from jmespath-community/jep/root-reference
Browse files Browse the repository at this point in the history
JEP-17 Root Reference
  • Loading branch information
springcomp authored Dec 8, 2022
2 parents 8dce507 + c9646f5 commit e21ac5c
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 22 deletions.
4 changes: 4 additions & 0 deletions jmespath/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ def current_node():
return {'type': 'current', 'children': []}


def root_node():
return {'type': 'root', 'children': []}


def expref(expression):
return {'type': 'expref', 'children': [expression]}

Expand Down
1 change: 1 addition & 0 deletions jmespath/lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Lexer(object):
',': 'comma',
':': 'colon',
'@': 'current',
'$': 'root',
'(': 'lparen',
')': 'rparen',
'{': 'lbrace',
Expand Down
12 changes: 8 additions & 4 deletions jmespath/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"""
import random


from jmespath import lexer
from jmespath.compat import with_repr_method
from jmespath import ast
Expand All @@ -46,6 +47,7 @@ class Parser(object):
'rbrace': 0,
'number': 0,
'current': 0,
'root': 0,
'expref': 0,
'colon': 0,
'pipe': 1,
Expand Down Expand Up @@ -239,6 +241,9 @@ def _parse_slice_expression(self):
def _token_nud_current(self, token):
return ast.current_node()

def _token_nud_root(self, token):
return ast.root_node()

def _token_nud_expref(self, token):
expression = self._expression(self.BINDING_POWER['expref'])
return ast.expref(expression)
Expand Down Expand Up @@ -505,9 +510,8 @@ def __init__(self, expression, parsed):
self.parsed = parsed

def search(self, value, options=None):
interpreter = visitor.ScopedInterpreter(options)
result = interpreter.visit(self.parsed, value)
return result
evaluator = visitor.ScopedInterpreter(options)
return evaluator.evaluate(self.parsed, value)

def _render_dot_file(self):
"""Render the parsed AST as a dot file.
Expand All @@ -524,4 +528,4 @@ def _render_dot_file(self):
return contents

def __repr__(self):
return repr(self.parsed)
return repr(self.parsed)
43 changes: 25 additions & 18 deletions jmespath/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from jmespath import functions
from jmespath.compat import string_type
from jmespath.compat import with_str_method
from numbers import Number


Expand Down Expand Up @@ -131,30 +132,23 @@ def visit_subexpression(self, node, value):
return result

def visit_field(self, node, value, *args, **kwargs):

identifier = node['value']

## inner function to retrieve the given
## value from the scopes stack

def get_value_from_current_context_or_scopes():
##try:
## return getattr(value, identifier)
##except AttributeError:
if 'scopes' in kwargs:
return kwargs['scopes'].getValue(identifier)
return None

## search for identifier value
scopes = kwargs.get('scopes')

try:
result = value.get(identifier)
if result == None:
result = get_value_from_current_context_or_scopes()
result = self._get_from_scopes(
identifier, *args, scopes=scopes)
return result
except AttributeError:
return get_value_from_current_context_or_scopes()
return self._get_from_scopes(
identifier, *args, scopes=scopes)

def _get_from_scopes(self, identifier, *args, **kwargs):
if 'scopes' in kwargs:
return kwargs['scopes'].getValue(identifier)
return None

def visit_comparator(self, node, value):
# Common case: comparator is == or !=
Expand All @@ -179,6 +173,11 @@ def visit_comparator(self, node, value):
def visit_current(self, node, value):
return value

def visit_root(self, *args, **kwargs):
if 'scopes' in kwargs:
return kwargs['scopes'].getValue('$')
return None

def visit_expref(self, node, value):
return _Expression(node['children'][0], self, value)

Expand Down Expand Up @@ -347,6 +346,7 @@ def _visit(self, node, current):
self._visit(child, child_name)


@with_str_method
class Scopes:
def __init__(self):
self._scopes = []
Expand All @@ -364,15 +364,22 @@ def getValue(self, identifier):
return scope[identifier]
return None

def __str__(self):
return '{}'.format(self._scopes)


class ScopedInterpreter(TreeInterpreter):
def __init__(self, options = None):
super().__init__(options)
self._scopes = Scopes()

def evaluate(self, ast, root_scope):
self._scopes.pushScope({'$': root_scope})
return self.visit(ast, root_scope)

def visit(self, node, *args, **kwargs):
node_type = node['type']
if (node_type in ['field', 'function_expression']):
scoped_types = ['field', 'function_expression', 'root']
if (node['type'] in scoped_types):
kwargs.update({'scopes': self._scopes})
else:
if 'scopes' in kwargs:
Expand Down
35 changes: 35 additions & 0 deletions tests/compliance/lexical_scoping.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,40 @@
"result": "fourth"
}
]
},
{
"given": {
"first_choice": "WA",
"states": [
{
"name": "WA",
"cities": [
"Seattle",
"Bellevue",
"Olympia"
]
},
{
"name": "CA",
"cities": [
"Los Angeles",
"San Francisco"
]
},
{
"name": "NY",
"cities": [
"New York City",
"Albany"
]
}
]
},
"cases": [
{
"expression": "states[?name==$.first_choice].cities[]",
"result": ["Seattle", "Bellevue", "Olympia"]
}
]
}
]
17 changes: 17 additions & 0 deletions tests/test_lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,23 @@ def test_adds_quotes_when_invalid_json(self):
]
)

def test_root_reference(self):
tokens = list(self.lexer.tokenize('$[0]'))
self.assertEqual(
tokens,
[{'type': 'root', 'value': '$',
'start': 0, 'end': 1},
{'type': 'lbracket', 'value':
'[', 'start': 1, 'end': 2},
{'type': 'number', 'value': 0,
'start': 2, 'end': 3},
{'type': 'rbracket', 'value': ']',
'start': 3, 'end': 4},
{'type': 'eof', 'value': '',
'start': 4, 'end': 4}
]
)

def test_unknown_character(self):
with self.assertRaises(LexerError) as e:
tokens = list(self.lexer.tokenize('foo[0^]'))
Expand Down
11 changes: 11 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ def test_function_call_with_and_statement(self):
'type': 'function_expression',
'value': 'f'})

def test_root_node(self):
self.assert_parsed_ast(
'$[0]',
{
'type': 'index_expression',
'children': [
{'type': 'root', 'children': []},
{'type': 'index', 'value': 0, 'children': []}
]
})


class TestErrorMessages(unittest.TestCase):

Expand Down

0 comments on commit e21ac5c

Please sign in to comment.