Skip to content

Commit e21ac5c

Browse files
authored
Merge pull request #8 from jmespath-community/jep/root-reference
JEP-17 Root Reference
2 parents 8dce507 + c9646f5 commit e21ac5c

File tree

7 files changed

+101
-22
lines changed

7 files changed

+101
-22
lines changed

jmespath/ast.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ def current_node():
1010
return {'type': 'current', 'children': []}
1111

1212

13+
def root_node():
14+
return {'type': 'root', 'children': []}
15+
16+
1317
def expref(expression):
1418
return {'type': 'expref', 'children': [expression]}
1519

jmespath/lexer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Lexer(object):
1717
',': 'comma',
1818
':': 'colon',
1919
'@': 'current',
20+
'$': 'root',
2021
'(': 'lparen',
2122
')': 'rparen',
2223
'{': 'lbrace',

jmespath/parser.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"""
2828
import random
2929

30+
3031
from jmespath import lexer
3132
from jmespath.compat import with_repr_method
3233
from jmespath import ast
@@ -46,6 +47,7 @@ class Parser(object):
4647
'rbrace': 0,
4748
'number': 0,
4849
'current': 0,
50+
'root': 0,
4951
'expref': 0,
5052
'colon': 0,
5153
'pipe': 1,
@@ -239,6 +241,9 @@ def _parse_slice_expression(self):
239241
def _token_nud_current(self, token):
240242
return ast.current_node()
241243

244+
def _token_nud_root(self, token):
245+
return ast.root_node()
246+
242247
def _token_nud_expref(self, token):
243248
expression = self._expression(self.BINDING_POWER['expref'])
244249
return ast.expref(expression)
@@ -505,9 +510,8 @@ def __init__(self, expression, parsed):
505510
self.parsed = parsed
506511

507512
def search(self, value, options=None):
508-
interpreter = visitor.ScopedInterpreter(options)
509-
result = interpreter.visit(self.parsed, value)
510-
return result
513+
evaluator = visitor.ScopedInterpreter(options)
514+
return evaluator.evaluate(self.parsed, value)
511515

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

526530
def __repr__(self):
527-
return repr(self.parsed)
531+
return repr(self.parsed)

jmespath/visitor.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from jmespath import functions
44
from jmespath.compat import string_type
5+
from jmespath.compat import with_str_method
56
from numbers import Number
67

78

@@ -131,30 +132,23 @@ def visit_subexpression(self, node, value):
131132
return result
132133

133134
def visit_field(self, node, value, *args, **kwargs):
134-
135135
identifier = node['value']
136-
137-
## inner function to retrieve the given
138-
## value from the scopes stack
139-
140-
def get_value_from_current_context_or_scopes():
141-
##try:
142-
## return getattr(value, identifier)
143-
##except AttributeError:
144-
if 'scopes' in kwargs:
145-
return kwargs['scopes'].getValue(identifier)
146-
return None
147-
148-
## search for identifier value
136+
scopes = kwargs.get('scopes')
149137

150138
try:
151139
result = value.get(identifier)
152140
if result == None:
153-
result = get_value_from_current_context_or_scopes()
141+
result = self._get_from_scopes(
142+
identifier, *args, scopes=scopes)
154143
return result
155144
except AttributeError:
156-
return get_value_from_current_context_or_scopes()
145+
return self._get_from_scopes(
146+
identifier, *args, scopes=scopes)
157147

148+
def _get_from_scopes(self, identifier, *args, **kwargs):
149+
if 'scopes' in kwargs:
150+
return kwargs['scopes'].getValue(identifier)
151+
return None
158152

159153
def visit_comparator(self, node, value):
160154
# Common case: comparator is == or !=
@@ -179,6 +173,11 @@ def visit_comparator(self, node, value):
179173
def visit_current(self, node, value):
180174
return value
181175

176+
def visit_root(self, *args, **kwargs):
177+
if 'scopes' in kwargs:
178+
return kwargs['scopes'].getValue('$')
179+
return None
180+
182181
def visit_expref(self, node, value):
183182
return _Expression(node['children'][0], self, value)
184183

@@ -347,6 +346,7 @@ def _visit(self, node, current):
347346
self._visit(child, child_name)
348347

349348

349+
@with_str_method
350350
class Scopes:
351351
def __init__(self):
352352
self._scopes = []
@@ -364,15 +364,22 @@ def getValue(self, identifier):
364364
return scope[identifier]
365365
return None
366366

367+
def __str__(self):
368+
return '{}'.format(self._scopes)
369+
367370

368371
class ScopedInterpreter(TreeInterpreter):
369372
def __init__(self, options = None):
370373
super().__init__(options)
371374
self._scopes = Scopes()
372375

376+
def evaluate(self, ast, root_scope):
377+
self._scopes.pushScope({'$': root_scope})
378+
return self.visit(ast, root_scope)
379+
373380
def visit(self, node, *args, **kwargs):
374-
node_type = node['type']
375-
if (node_type in ['field', 'function_expression']):
381+
scoped_types = ['field', 'function_expression', 'root']
382+
if (node['type'] in scoped_types):
376383
kwargs.update({'scopes': self._scopes})
377384
else:
378385
if 'scopes' in kwargs:

tests/compliance/lexical_scoping.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,5 +166,40 @@
166166
"result": "fourth"
167167
}
168168
]
169+
},
170+
{
171+
"given": {
172+
"first_choice": "WA",
173+
"states": [
174+
{
175+
"name": "WA",
176+
"cities": [
177+
"Seattle",
178+
"Bellevue",
179+
"Olympia"
180+
]
181+
},
182+
{
183+
"name": "CA",
184+
"cities": [
185+
"Los Angeles",
186+
"San Francisco"
187+
]
188+
},
189+
{
190+
"name": "NY",
191+
"cities": [
192+
"New York City",
193+
"Albany"
194+
]
195+
}
196+
]
197+
},
198+
"cases": [
199+
{
200+
"expression": "states[?name==$.first_choice].cities[]",
201+
"result": ["Seattle", "Bellevue", "Olympia"]
202+
}
203+
]
169204
}
170205
]

tests/test_lexer.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,23 @@ def test_adds_quotes_when_invalid_json(self):
143143
]
144144
)
145145

146+
def test_root_reference(self):
147+
tokens = list(self.lexer.tokenize('$[0]'))
148+
self.assertEqual(
149+
tokens,
150+
[{'type': 'root', 'value': '$',
151+
'start': 0, 'end': 1},
152+
{'type': 'lbracket', 'value':
153+
'[', 'start': 1, 'end': 2},
154+
{'type': 'number', 'value': 0,
155+
'start': 2, 'end': 3},
156+
{'type': 'rbracket', 'value': ']',
157+
'start': 3, 'end': 4},
158+
{'type': 'eof', 'value': '',
159+
'start': 4, 'end': 4}
160+
]
161+
)
162+
146163
def test_unknown_character(self):
147164
with self.assertRaises(LexerError) as e:
148165
tokens = list(self.lexer.tokenize('foo[0^]'))

tests/test_parser.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,17 @@ def test_function_call_with_and_statement(self):
106106
'type': 'function_expression',
107107
'value': 'f'})
108108

109+
def test_root_node(self):
110+
self.assert_parsed_ast(
111+
'$[0]',
112+
{
113+
'type': 'index_expression',
114+
'children': [
115+
{'type': 'root', 'children': []},
116+
{'type': 'index', 'value': 0, 'children': []}
117+
]
118+
})
119+
109120

110121
class TestErrorMessages(unittest.TestCase):
111122

0 commit comments

Comments
 (0)