Skip to content

Commit b0718c7

Browse files
committed
Merge branch 'better-filters' into develop
Implements JEP-9. Closes #96. Fixes #83. * better-filters: Fix failing test Add failing test Incorporate new JEP-9 tokens into new lexer Allow for newlines within tokens Initial commit of improved filters (JEP 9)
2 parents 8db8f11 + 360e742 commit b0718c7

File tree

10 files changed

+483
-79
lines changed

10 files changed

+483
-79
lines changed

jmespath/ast.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ def or_expression(left, right):
6262
return {"type": "or_expression", "children": [left, right]}
6363

6464

65+
def and_expression(left, right):
66+
return {"type": "and_expression", "children": [left, right]}
67+
68+
69+
def not_expression(expr):
70+
return {"type": "not_expression", "children": [expr]}
71+
72+
6573
def pipe(left, right):
6674
return {'type': 'pipe', 'children': [left, right]}
6775

jmespath/lexer.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@ class Lexer(object):
1818
',': 'comma',
1919
':': 'colon',
2020
'@': 'current',
21-
'&': 'expref',
2221
'(': 'lparen',
2322
')': 'rparen',
2423
'{': 'lbrace',
25-
'}': 'rbrace'
24+
'}': 'rbrace',
2625
}
2726

2827
def tokenize(self, expression):
@@ -60,6 +59,8 @@ def tokenize(self, expression):
6059
yield self._consume_raw_string_literal()
6160
elif self._current == '|':
6261
yield self._match_or_else('|', 'or', 'pipe')
62+
elif self._current == '&':
63+
yield self._match_or_else('&', 'and', 'expref')
6364
elif self._current == '`':
6465
yield self._consume_literal()
6566
elif self._current in self.START_NUMBER:
@@ -76,7 +77,7 @@ def tokenize(self, expression):
7677
elif self._current == '>':
7778
yield self._match_or_else('=', 'gte', 'gt')
7879
elif self._current == '!':
79-
yield self._match_or_else('=', 'ne', 'unknown')
80+
yield self._match_or_else('=', 'ne', 'not')
8081
elif self._current == '=':
8182
yield self._match_or_else('=', 'eq', 'unknown')
8283
else:

jmespath/parser.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,27 @@ class Parser(object):
4848
'expref': 0,
4949
'colon': 0,
5050
'pipe': 1,
51-
'eq': 2,
52-
'gt': 2,
53-
'lt': 2,
54-
'gte': 2,
55-
'lte': 2,
56-
'ne': 2,
57-
'or': 5,
58-
'flatten': 6,
51+
'or': 2,
52+
'and': 3,
53+
'eq': 5,
54+
'gt': 5,
55+
'lt': 5,
56+
'gte': 5,
57+
'lte': 5,
58+
'ne': 5,
59+
'flatten': 9,
60+
# Everything above stops a projection.
5961
'star': 20,
6062
'filter': 21,
6163
'dot': 40,
64+
'not': 45,
6265
'lbrace': 50,
6366
'lbracket': 55,
6467
'lparen': 60,
6568
}
69+
# The maximum binding power for a token that can stop
70+
# a projection.
71+
_PROJECTION_STOP = 10
6672
# The _MAX_SIZE most recent expressions are cached in
6773
# _CACHE dict.
6874
_CACHE = {}
@@ -161,12 +167,21 @@ def _token_nud_filter(self, token):
161167
def _token_nud_lbrace(self, token):
162168
return self._parse_multi_select_hash()
163169

170+
def _token_nud_lparen(self, token):
171+
expression = self._expression()
172+
self._match('rparen')
173+
return expression
174+
164175
def _token_nud_flatten(self, token):
165176
left = ast.flatten(ast.identity())
166177
right = self._parse_projection_rhs(
167178
self.BINDING_POWER['flatten'])
168179
return ast.projection(left, right)
169180

181+
def _token_nud_not(self, token):
182+
expr = self._expression(self.BINDING_POWER['not'])
183+
return ast.not_expression(expr)
184+
170185
def _token_nud_lbracket(self, token):
171186
if self._current_token() in ['number', 'colon']:
172187
right = self._parse_index_expression()
@@ -254,15 +269,15 @@ def _token_led_or(self, left):
254269
right = self._expression(self.BINDING_POWER['or'])
255270
return ast.or_expression(left, right)
256271

272+
def _token_led_and(self, left):
273+
right = self._expression(self.BINDING_POWER['and'])
274+
return ast.and_expression(left, right)
275+
257276
def _token_led_lparen(self, left):
258277
name = left['value']
259278
args = []
260279
while not self._current_token() == 'rparen':
261-
if self._current_token() == 'current':
262-
expression = ast.current_node()
263-
self._advance()
264-
else:
265-
expression = self._expression()
280+
expression = self._expression()
266281
if self._current_token() == 'comma':
267282
self._match('comma')
268283
args.append(expression)
@@ -370,7 +385,7 @@ def _parse_multi_select_hash(self):
370385

371386
def _parse_projection_rhs(self, binding_power):
372387
# Parse the right hand side of the projection.
373-
if self.BINDING_POWER[self._current_token()] < 10:
388+
if self.BINDING_POWER[self._current_token()] < self._PROJECTION_STOP:
374389
# BP of 10 are all the tokens that stop a projection.
375390
right = ast.identity()
376391
elif self._current_token() == 'lbracket':

jmespath/visitor.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ def __init__(self, options=None):
9393
# properly freed.
9494
self._functions.interpreter = self
9595

96+
def default_visit(self, node, *args, **kwargs):
97+
raise NotImplementedError(node['type'])
98+
9699
def visit_subexpression(self, node, value):
97100
result = value
98101
for node in node['children']:
@@ -132,7 +135,7 @@ def visit_filter_projection(self, node, value):
132135
comparator_node = node['children'][2]
133136
collected = []
134137
for element in base:
135-
if self.visit(comparator_node, element):
138+
if self._is_true(self.visit(comparator_node, element)):
136139
current = self.visit(node['children'][1], element)
137140
if current is not None:
138141
collected.append(current)
@@ -204,6 +207,20 @@ def visit_or_expression(self, node, value):
204207
matched = self.visit(node['children'][1], value)
205208
return matched
206209

210+
def visit_and_expression(self, node, value):
211+
matched = self.visit(node['children'][0], value)
212+
if self._is_false(matched):
213+
return matched
214+
return self.visit(node['children'][1], value)
215+
216+
def visit_not_expression(self, node, value):
217+
original_result = self.visit(node['children'][0], value)
218+
if original_result is 0:
219+
# Special case for 0, !0 should be false, not true.
220+
# 0 is not a special cased integer in jmespath.
221+
return False
222+
return not original_result
223+
207224
def visit_pipe(self, node, value):
208225
result = value
209226
for node in node['children']:
@@ -241,6 +258,9 @@ def _is_false(self, value):
241258
return (value == '' or value == [] or value == {} or value is None or
242259
value is False)
243260

261+
def _is_true(self, value):
262+
return not self._is_false(value)
263+
244264

245265
class GraphvizVisitor(Visitor):
246266
def __init__(self):

tests/compliance/basic.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
"expression": "foo.bar.baz",
1515
"result": "correct"
1616
},
17+
{
18+
"expression": "foo\n.\nbar\n.baz",
19+
"result": "correct"
20+
},
1721
{
1822
"expression": "foo.bar.baz.bad",
1923
"result": null

0 commit comments

Comments
 (0)