Skip to content

Commit 830e245

Browse files
committed
Merge tag 'JEP-17' into feature/vnext-preview
2 parents 5094f1d + cdb57fa commit 830e245

File tree

7 files changed

+100
-24
lines changed

7 files changed

+100
-24
lines changed

jmespath/ast.py

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

2020

21+
def root_node():
22+
return {'type': 'root', 'children': []}
23+
24+
2125
def expref(expression):
2226
return {'type': 'expref', 'children': [expression]}
2327

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: 7 additions & 6 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,
@@ -251,6 +253,9 @@ def _parse_slice_expression(self):
251253
def _token_nud_current(self, token):
252254
return ast.current_node()
253255

256+
def _token_nud_root(self, token):
257+
return ast.root_node()
258+
254259
def _token_nud_expref(self, token):
255260
expression = self._expression(self.BINDING_POWER['expref'])
256261
return ast.expref(expression)
@@ -546,9 +551,8 @@ def __init__(self, expression, parsed):
546551
self.parsed = parsed
547552

548553
def search(self, value, options=None):
549-
interpreter = visitor.ScopedInterpreter(options)
550-
result = interpreter.visit(self.parsed, value)
551-
return result
554+
evaluator = visitor.ScopedInterpreter(options)
555+
return evaluator.evaluate(self.parsed, value)
552556

553557
def _render_dot_file(self):
554558
"""Render the parsed AST as a dot file.
@@ -563,6 +567,3 @@ def _render_dot_file(self):
563567
renderer = visitor.GraphvizVisitor()
564568
contents = renderer.visit(self.parsed)
565569
return contents
566-
567-
def __repr__(self):
568-
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

@@ -143,30 +144,23 @@ def visit_subexpression(self, node, value):
143144
return result
144145

145146
def visit_field(self, node, value, *args, **kwargs):
146-
147147
identifier = node['value']
148-
149-
## inner function to retrieve the given
150-
## value from the scopes stack
151-
152-
def get_value_from_current_context_or_scopes():
153-
##try:
154-
## return getattr(value, identifier)
155-
##except AttributeError:
156-
if 'scopes' in kwargs:
157-
return kwargs['scopes'].getValue(identifier)
158-
return None
159-
160-
## search for identifier value
148+
scopes = kwargs.get('scopes')
161149

162150
try:
163151
result = value.get(identifier)
164152
if result == None:
165-
result = get_value_from_current_context_or_scopes()
153+
result = self._get_from_scopes(
154+
identifier, *args, scopes=scopes)
166155
return result
167156
except AttributeError:
168-
return get_value_from_current_context_or_scopes()
157+
return self._get_from_scopes(
158+
identifier, *args, scopes=scopes)
169159

160+
def _get_from_scopes(self, identifier, *args, **kwargs):
161+
if 'scopes' in kwargs:
162+
return kwargs['scopes'].getValue(identifier)
163+
return None
170164

171165
def visit_comparator(self, node, value):
172166
# Common case: comparator is == or !=
@@ -204,6 +198,11 @@ def visit_arithmetic(self, node, value):
204198
def visit_current(self, node, value):
205199
return value
206200

201+
def visit_root(self, *args, **kwargs):
202+
if 'scopes' in kwargs:
203+
return kwargs['scopes'].getValue('$')
204+
return None
205+
207206
def visit_expref(self, node, value):
208207
return _Expression(node['children'][0], self, value)
209208

@@ -372,6 +371,7 @@ def _visit(self, node, current):
372371
self._visit(child, child_name)
373372

374373

374+
@with_str_method
375375
class Scopes:
376376
def __init__(self):
377377
self._scopes = []
@@ -389,15 +389,22 @@ def getValue(self, identifier):
389389
return scope[identifier]
390390
return None
391391

392+
def __str__(self):
393+
return '{}'.format(self._scopes)
394+
392395

393396
class ScopedInterpreter(TreeInterpreter):
394397
def __init__(self, options = None):
395398
super().__init__(options)
396399
self._scopes = Scopes()
397400

401+
def evaluate(self, ast, root_scope):
402+
self._scopes.pushScope({'$': root_scope})
403+
return self.visit(ast, root_scope)
404+
398405
def visit(self, node, *args, **kwargs):
399-
node_type = node['type']
400-
if (node_type in ['field', 'function_expression']):
406+
scoped_types = ['field', 'function_expression', 'root']
407+
if (node['type'] in scoped_types):
401408
kwargs.update({'scopes': self._scopes})
402409
else:
403410
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
@@ -187,6 +187,23 @@ def test_adds_quotes_when_invalid_json(self):
187187
]
188188
)
189189

190+
def test_root_reference(self):
191+
tokens = list(self.lexer.tokenize('$[0]'))
192+
self.assertEqual(
193+
tokens,
194+
[{'type': 'root', 'value': '$',
195+
'start': 0, 'end': 1},
196+
{'type': 'lbracket', 'value':
197+
'[', 'start': 1, 'end': 2},
198+
{'type': 'number', 'value': 0,
199+
'start': 2, 'end': 3},
200+
{'type': 'rbracket', 'value': ']',
201+
'start': 3, 'end': 4},
202+
{'type': 'eof', 'value': '',
203+
'start': 4, 'end': 4}
204+
]
205+
)
206+
190207
def test_unknown_character(self):
191208
with self.assertRaises(LexerError) as e:
192209
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
@@ -156,6 +156,17 @@ def test_function_call_with_and_statement(self):
156156
'type': 'function_expression',
157157
'value': 'f'})
158158

159+
def test_root_node(self):
160+
self.assert_parsed_ast(
161+
'$[0]',
162+
{
163+
'type': 'index_expression',
164+
'children': [
165+
{'type': 'root', 'children': []},
166+
{'type': 'index', 'value': 0, 'children': []}
167+
]
168+
})
169+
159170

160171
class TestErrorMessages(unittest.TestCase):
161172

0 commit comments

Comments
 (0)