Skip to content

Commit 255ecc3

Browse files
committed
Merge tag 'JEP-11' into feature/vnext-preview
2 parents 55d3e56 + 8dce507 commit 255ecc3

File tree

5 files changed

+276
-11
lines changed

5 files changed

+276
-11
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: 62 additions & 9 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)
@@ -326,3 +345,37 @@ def _visit(self, node, current):
326345
self._count += 1
327346
self._lines.append(' %s -> %s' % (current, child_name))
328347
self._visit(child, child_name)
348+
349+
350+
class Scopes:
351+
def __init__(self):
352+
self._scopes = []
353+
354+
def pushScope(self, scope):
355+
self._scopes.append(scope)
356+
357+
def popScope(self):
358+
if len(self._scopes) > 0:
359+
self._scopes.pop()
360+
361+
def getValue(self, identifier):
362+
for scope in self._scopes[::-1]:
363+
if scope.get(identifier) != None:
364+
return scope[identifier]
365+
return None
366+
367+
368+
class ScopedInterpreter(TreeInterpreter):
369+
def __init__(self, options = None):
370+
super().__init__(options)
371+
self._scopes = Scopes()
372+
373+
def visit(self, node, *args, **kwargs):
374+
node_type = node['type']
375+
if (node_type in ['field', 'function_expression']):
376+
kwargs.update({'scopes': self._scopes})
377+
else:
378+
if 'scopes' in kwargs:
379+
kwargs.pop('scopes')
380+
381+
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+
]

tests/test_scopes.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
import pytest
3+
4+
from tests import unittest
5+
from jmespath.visitor import Scopes
6+
7+
class TestScope(unittest.TestCase):
8+
def setUp(self):
9+
self._scopes = Scopes()
10+
11+
def test_Scope_missing(self):
12+
value = self._scopes.getValue('foo')
13+
self.assertEqual(None, value)
14+
15+
def test_Scope_root(self):
16+
self._scopes.pushScope({'foo': 'bar'})
17+
value = self._scopes.getValue('foo')
18+
self.assertEqual('bar', value)
19+
20+
def test_Scope_nested(self):
21+
self._scopes.pushScope({'foo': 'bar'})
22+
self._scopes.pushScope({'foo': 'baz'})
23+
value = self._scopes.getValue('foo')
24+
self.assertEqual('baz', value)
25+
26+
if __name__ == '__main__':
27+
unittest.main()

0 commit comments

Comments
 (0)