Skip to content

Commit f324de9

Browse files
authored
Merge pull request #7 from jmespath-community/jep/lexical-scoping
JEP-11 Lexical Scoping
2 parents 827ac0d + 8db8bae commit f324de9

File tree

9 files changed

+358
-14
lines changed

9 files changed

+358
-14
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/functions.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import math
22
import json
3+
import inspect
4+
from pydoc import resolve
35

46
from collections import OrderedDict
57

@@ -73,7 +75,7 @@ class Functions(metaclass=FunctionRegistry):
7375
FUNCTION_TABLE = {
7476
}
7577

76-
def call_function(self, function_name, resolved_args):
78+
def call_function(self, function_name, resolved_args, *args, **kwargs):
7779
try:
7880
spec = self.FUNCTION_TABLE[function_name]
7981
except KeyError:
@@ -82,6 +84,13 @@ def call_function(self, function_name, resolved_args):
8284
function = spec['function']
8385
signature = spec['signature']
8486
self._validate_arguments(resolved_args, signature, function_name)
87+
88+
# supply extra arguments only if the function expects them
89+
90+
parameters = [parameter.name for parameter in inspect.signature(function).parameters.values()]
91+
if ('kwargs' in parameters):
92+
return function(self, *resolved_args, *args, scopes = kwargs.get('scopes'))
93+
8594
return function(self, *resolved_args)
8695

8796
def _validate_arguments(self, args, signature, function_name):
@@ -531,6 +540,12 @@ def _func_max_by(self, array, expref):
531540
else:
532541
return None
533542

543+
@signature({'types': ['object']}, {'types': ['expref']})
544+
def _func_let(self, scope, expref, *args, **kwargs):
545+
if 'scopes' in kwargs:
546+
kwargs.get('scopes').pushScope(scope)
547+
return expref.visit(expref.expression, expref.context, *args, **kwargs)
548+
534549
@signature({'types': ['array'], 'variadic': True})
535550
def _func_zip(self, *arguments):
536551
return list(map(list, zip(*arguments)))

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,
@@ -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.TreeInterpreter(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.
@@ -565,4 +569,4 @@ def _render_dot_file(self):
565569
return contents
566570

567571
def __repr__(self):
568-
return repr(self.parsed)
572+
return repr(self.parsed)

jmespath/visitor.py

Lines changed: 69 additions & 9 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

@@ -72,14 +73,14 @@ def __init__(self, dict_cls=None, custom_functions=None):
7273

7374

7475
class _Expression(object):
75-
def __init__(self, expression, interpreter):
76+
def __init__(self, expression, interpreter, context):
7677
self.expression = expression
7778
self.interpreter = interpreter
79+
self.context = context
7880

7981
def visit(self, node, *args, **kwargs):
8082
return self.interpreter.visit(node, *args, **kwargs)
8183

82-
8384
class Visitor(object):
8485
def __init__(self):
8586
self._method_cache = {}
@@ -96,7 +97,6 @@ def visit(self, node, *args, **kwargs):
9697
def default_visit(self, node, *args, **kwargs):
9798
raise NotImplementedError("default_visit")
9899

99-
100100
class TreeInterpreter(Visitor):
101101
COMPARATOR_FUNC = {
102102
'eq': _equals,
@@ -145,11 +145,24 @@ def visit_subexpression(self, node, value):
145145
return None
146146
return result
147147

148-
def visit_field(self, node, value):
148+
def visit_field(self, node, value, *args, **kwargs):
149+
identifier = node['value']
150+
scopes = kwargs.get('scopes')
151+
149152
try:
150-
return value.get(node['value'])
153+
result = value.get(identifier)
154+
if result == None:
155+
result = self._get_from_scopes(
156+
identifier, *args, scopes=scopes)
157+
return result
151158
except AttributeError:
152-
return None
159+
return self._get_from_scopes(
160+
identifier, *args, scopes=scopes)
161+
162+
def _get_from_scopes(self, identifier, *args, **kwargs):
163+
if 'scopes' in kwargs:
164+
return kwargs['scopes'].getValue(identifier)
165+
return None
153166

154167
def visit_comparator(self, node, value):
155168
# Common case: comparator is == or !=
@@ -187,15 +200,20 @@ def visit_arithmetic(self, node, value):
187200
def visit_current(self, node, value):
188201
return value
189202

203+
def visit_root(self, *args, **kwargs):
204+
if 'scopes' in kwargs:
205+
return kwargs['scopes'].getValue('$')
206+
return None
207+
190208
def visit_expref(self, node, value):
191-
return _Expression(node['children'][0], self)
209+
return _Expression(node['children'][0], self, value)
192210

193-
def visit_function_expression(self, node, value):
211+
def visit_function_expression(self, node, value, *args, **kwargs):
194212
resolved_args = []
195213
for child in node['children']:
196214
current = self.visit(child, value)
197215
resolved_args.append(current)
198-
return self._functions.call_function(node['value'], resolved_args)
216+
return self._functions.call_function(node['value'], resolved_args, scopes = kwargs.get('scopes'))
199217

200218
def visit_filter_projection(self, node, value):
201219
base = self.visit(node['children'][0], value)
@@ -366,3 +384,45 @@ def _visit(self, node, current):
366384
self._count += 1
367385
self._lines.append(' %s -> %s' % (current, child_name))
368386
self._visit(child, child_name)
387+
388+
389+
@with_str_method
390+
class Scopes:
391+
def __init__(self):
392+
self._scopes = []
393+
394+
def pushScope(self, scope):
395+
self._scopes.append(scope)
396+
397+
def popScope(self):
398+
if len(self._scopes) > 0:
399+
self._scopes.pop()
400+
401+
def getValue(self, identifier):
402+
for scope in self._scopes[::-1]:
403+
if scope.get(identifier) != None:
404+
return scope[identifier]
405+
return None
406+
407+
def __str__(self):
408+
return '{}'.format(self._scopes)
409+
410+
411+
class ScopedInterpreter(TreeInterpreter):
412+
def __init__(self, options = None):
413+
super().__init__(options)
414+
self._scopes = Scopes()
415+
416+
def evaluate(self, ast, root_scope):
417+
self._scopes.pushScope({'$': root_scope})
418+
return self.visit(ast, root_scope)
419+
420+
def visit(self, node, *args, **kwargs):
421+
scoped_types = ['field', 'function_expression', 'root']
422+
if (node['type'] in scoped_types):
423+
kwargs.update({'scopes': self._scopes})
424+
else:
425+
if 'scopes' in kwargs:
426+
kwargs.pop('scopes')
427+
428+
return super().visit(node, *args, **kwargs)

0 commit comments

Comments
 (0)