Skip to content

Commit 8dce507

Browse files
committed
Implements let() function.
1 parent 1443207 commit 8dce507

File tree

4 files changed

+219
-12
lines changed

4 files changed

+219
-12
lines changed

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 jmespath import exceptions
57
from jmespath.compat import string_type as STRING_TYPE
@@ -69,7 +71,7 @@ class Functions(metaclass=FunctionRegistry):
6971
FUNCTION_TABLE = {
7072
}
7173

72-
def call_function(self, function_name, resolved_args):
74+
def call_function(self, function_name, resolved_args, *args, **kwargs):
7375
try:
7476
spec = self.FUNCTION_TABLE[function_name]
7577
except KeyError:
@@ -78,6 +80,13 @@ def call_function(self, function_name, resolved_args):
7880
function = spec['function']
7981
signature = spec['signature']
8082
self._validate_arguments(resolved_args, signature, function_name)
83+
84+
# supply extra arguments only if the function expects them
85+
86+
parameters = [parameter.name for parameter in inspect.signature(function).parameters.values()]
87+
if ('kwargs' in parameters):
88+
return function(self, *resolved_args, *args, scopes = kwargs.get('scopes'))
89+
8190
return function(self, *resolved_args)
8291

8392
def _validate_arguments(self, args, signature, function_name):
@@ -346,6 +355,12 @@ def _func_max_by(self, array, expref):
346355
else:
347356
return None
348357

358+
@signature({'types': ['object']}, {'types': ['expref']})
359+
def _func_let(self, scope, expref, *args, **kwargs):
360+
if 'scopes' in kwargs:
361+
kwargs.get('scopes').pushScope(scope)
362+
return expref.visit(expref.expression, expref.context, *args, **kwargs)
363+
349364
def _create_key_func(self, expref, allowed_types, function_name):
350365
def keyfunc(x):
351366
result = expref.visit(expref.expression, x)

jmespath/parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ def __init__(self, expression, parsed):
505505
self.parsed = parsed
506506

507507
def search(self, value, options=None):
508-
interpreter = visitor.TreeInterpreter(options)
508+
interpreter = visitor.ScopedInterpreter(options)
509509
result = interpreter.visit(self.parsed, value)
510510
return result
511511

jmespath/visitor.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,14 @@ def __init__(self, dict_cls=None, custom_functions=None):
7272

7373

7474
class _Expression(object):
75-
def __init__(self, expression, interpreter):
75+
def __init__(self, expression, interpreter, context):
7676
self.expression = expression
7777
self.interpreter = interpreter
78+
self.context = context
7879

7980
def visit(self, node, *args, **kwargs):
8081
return self.interpreter.visit(node, *args, **kwargs)
8182

82-
8383
class Visitor(object):
8484
def __init__(self):
8585
self._method_cache = {}
@@ -96,7 +96,6 @@ def visit(self, node, *args, **kwargs):
9696
def default_visit(self, node, *args, **kwargs):
9797
raise NotImplementedError("default_visit")
9898

99-
10099
class TreeInterpreter(Visitor):
101100
COMPARATOR_FUNC = {
102101
'eq': _equals,
@@ -131,11 +130,31 @@ def visit_subexpression(self, node, value):
131130
result = self.visit(node, result)
132131
return result
133132

134-
def visit_field(self, node, value):
133+
def visit_field(self, node, value, *args, **kwargs):
134+
135+
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
149+
135150
try:
136-
return value.get(node['value'])
151+
result = value.get(identifier)
152+
if result == None:
153+
result = get_value_from_current_context_or_scopes()
154+
return result
137155
except AttributeError:
138-
return None
156+
return get_value_from_current_context_or_scopes()
157+
139158

140159
def visit_comparator(self, node, value):
141160
# Common case: comparator is == or !=
@@ -161,14 +180,14 @@ def visit_current(self, node, value):
161180
return value
162181

163182
def visit_expref(self, node, value):
164-
return _Expression(node['children'][0], self)
183+
return _Expression(node['children'][0], self, value)
165184

166-
def visit_function_expression(self, node, value):
185+
def visit_function_expression(self, node, value, *args, **kwargs):
167186
resolved_args = []
168187
for child in node['children']:
169188
current = self.visit(child, value)
170189
resolved_args.append(current)
171-
return self._functions.call_function(node['value'], resolved_args)
190+
return self._functions.call_function(node['value'], resolved_args, scopes = kwargs.get('scopes'))
172191

173192
def visit_filter_projection(self, node, value):
174193
base = self.visit(node['children'][0], value)
@@ -353,7 +372,10 @@ def __init__(self, options = None):
353372

354373
def visit(self, node, *args, **kwargs):
355374
node_type = node['type']
356-
if (node_type == 'field'):
375+
if (node_type in ['field', 'function_expression']):
357376
kwargs.update({'scopes': self._scopes})
377+
else:
378+
if 'scopes' in kwargs:
379+
kwargs.pop('scopes')
358380

359381
return super().visit(node, *args, **kwargs)

tests/compliance/lexical_scoping.json

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
[
2+
{
3+
"given": {
4+
"search_for": "foo",
5+
"people": [
6+
{
7+
"name": "a"
8+
},
9+
{
10+
"name": "b"
11+
},
12+
{
13+
"name": "c"
14+
},
15+
{
16+
"name": "foo"
17+
},
18+
{
19+
"name": "bar"
20+
},
21+
{
22+
"name": "baz"
23+
},
24+
{
25+
"name": "qux"
26+
},
27+
{
28+
"name": "x"
29+
},
30+
{
31+
"name": "y"
32+
},
33+
{
34+
"name": "z"
35+
}
36+
]
37+
},
38+
"cases": [
39+
{
40+
"description": "Let function with filters",
41+
"expression": "let({search_for: search_for}, &people[?name==search_for].name | [0])",
42+
"result": "foo"
43+
}
44+
]
45+
},
46+
{
47+
"given": {
48+
"a": {
49+
"mylist": [
50+
{
51+
"l1": "1",
52+
"result": "foo"
53+
},
54+
{
55+
"l2": "2",
56+
"result": "bar"
57+
},
58+
{
59+
"l1": "8",
60+
"l2": "9"
61+
},
62+
{
63+
"l1": "8",
64+
"l2": "9"
65+
}
66+
],
67+
"level2": "2"
68+
},
69+
"level1": "1",
70+
"nested": {
71+
"a": {
72+
"b": {
73+
"c": {
74+
"fourth": "fourth"
75+
},
76+
"third": "third"
77+
},
78+
"second": "second"
79+
},
80+
"first": "first"
81+
},
82+
"precedence": {
83+
"a": {
84+
"b": {
85+
"c": {
86+
"variable": "fourth"
87+
},
88+
"variable": "third",
89+
"other": "y"
90+
},
91+
"variable": "second",
92+
"other": "x"
93+
},
94+
"variable": "first",
95+
"other": "w"
96+
}
97+
},
98+
"cases": [
99+
{
100+
"description": "Basic let from scope",
101+
"expression": "let({level1: level1}, &a.[level2, level1])",
102+
"result": [
103+
"2",
104+
"1"
105+
]
106+
},
107+
{
108+
"description": "Current object has precedence",
109+
"expression": "let({level1: `\"other\"`}, &level1)",
110+
"result": "1"
111+
},
112+
{
113+
"description": "No scope specified using literal hash",
114+
"expression": "let(`{}`, &a.level2)",
115+
"result": "2"
116+
},
117+
{
118+
"description": "Arbitrary variable added",
119+
"expression": "let({foo: `\"anything\"`}, &[level1, foo])",
120+
"result": [
121+
"1",
122+
"anything"
123+
]
124+
},
125+
{
126+
"description": "Basic let from current object",
127+
"expression": "let({other: level1}, &level1)",
128+
"result": "1"
129+
},
130+
{
131+
"description": "Nested let function with filters",
132+
"expression": "let({level1: level1}, &a.[mylist[?l1==level1].result, let({level2: level2}, &mylist[?l2==level2].result)])[]",
133+
"result": [
134+
"foo",
135+
"bar"
136+
]
137+
},
138+
{
139+
"description": "Nested let function with filters with literal scope binding",
140+
"expression": "let(`{\"level1\": \"1\"}`, &a.[mylist[?l1==level1].result, let({level2: level2}, &mylist[?l2==level2].result)])[]",
141+
"result": [
142+
"foo",
143+
"bar"
144+
]
145+
},
146+
{
147+
"description": "Nested let functions",
148+
"expression": "nested.let({level1: first}, &a.let({level2: second}, &b.let({level3: third}, &c.{first: level1, second: level2, third: level3, fourth: fourth})))",
149+
"result": {
150+
"first": "first",
151+
"second": "second",
152+
"third": "third",
153+
"fourth": "fourth"
154+
}
155+
},
156+
{
157+
"description": "Precedence of lexical vars from scope object",
158+
"expression": "precedence.let({other: other}, &a.let({other: other}, &b.let({other: other}, &c.{other: other})))",
159+
"result": {
160+
"other": "y"
161+
}
162+
},
163+
{
164+
"description": "Precedence of lexical vars from current object",
165+
"expression": "precedence.let({variable: variable}, &a.let({variable: variable}, &b.let({variable: variable}, &c.let({variable: `\"override\"`}, &variable))))",
166+
"result": "fourth"
167+
}
168+
]
169+
}
170+
]

0 commit comments

Comments
 (0)