Skip to content

Commit 882c00c

Browse files
authored
Merge branch 'develop' into jep/grouping
2 parents a4fec3f + e62713d commit 882c00c

17 files changed

+767
-25
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
*.py[cod]
22

3+
# pyvenv
4+
bin/
5+
share/
6+
pyvenv.cfg
7+
38
# C extensions
49
*.so
510

bin/jp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ def main():
4242
except exceptions.JMESPathTypeError as e:
4343
sys.stderr.write("invalid-type: %s\n" % e)
4444
return 1
45+
except exceptions.JMESPathValueError as e:
46+
sys.stderr.write("invalid-value: %s\n" % e)
47+
return 1
4548
except exceptions.UnknownFunctionError as e:
4649
sys.stderr.write("unknown-function: %s\n" % e)
4750
return 1

jmespath/ast.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
# {"type": <node type>", children: [], "value": ""}
33

44

5+
def arithmetic_unary(operator, expression):
6+
return {'type': 'arithmetic_unary', 'children': [expression], 'value': operator}
7+
8+
9+
def arithmetic(operator, left, right):
10+
return {'type': 'arithmetic', 'children': [left, right], 'value': operator}
11+
12+
513
def comparator(name, first, second):
614
return {'type': 'comparator', 'children': [first, second], 'value': name}
715

jmespath/compat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import sys
21
import inspect
3-
from itertools import zip_longest
42

3+
iteritems = dict.items
54

5+
map = map
66
text_type = str
77
string_type = str
88

jmespath/exceptions.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,19 @@ def __str__(self):
112112
self.expected_types, self.actual_type))
113113

114114

115+
@with_str_method
116+
class JMESPathValueError(JMESPathError):
117+
def __init__(self, function_name, current_value, expected_types):
118+
self.function_name = function_name
119+
self.current_value = current_value
120+
self.expected_types = expected_types
121+
122+
def __str__(self):
123+
return ('In function %s(), invalid value: "%s", '
124+
'expected: %s"%s"' % (
125+
self.function_name, self.current_value,
126+
self.expected_types))
127+
115128
class EmptyExpressionError(JMESPathError):
116129
def __init__(self):
117130
super(EmptyExpressionError, self).__init__(

jmespath/functions.py

Lines changed: 193 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from collections import OrderedDict
55

66
from jmespath import exceptions
7-
from jmespath.compat import string_type as STRING_TYPE
87
from jmespath.compat import get_methods
8+
from jmespath.compat import iteritems
9+
from jmespath.compat import map
10+
from jmespath.compat import string_type as STRING_TYPE
911

1012

1113
# python types -> jmespath types
@@ -83,22 +85,28 @@ def call_function(self, function_name, resolved_args):
8385
return function(self, *resolved_args)
8486

8587
def _validate_arguments(self, args, signature, function_name):
86-
if signature and signature[-1].get('variadic'):
88+
required_arguments_count = len([param for param in signature if not param.get('optional') or not param['optional']])
89+
optional_arguments_count = len([param for param in signature if param.get('optional') and param['optional']])
90+
has_variadic = signature[-1].get('variadic') if signature != None else False
91+
if has_variadic:
8792
if len(args) < len(signature):
8893
raise exceptions.VariadictArityError(
8994
len(signature), len(args), function_name)
90-
elif len(args) != len(signature):
95+
elif optional_arguments_count > 0:
96+
if len(args) < required_arguments_count or len(args) > (required_arguments_count + optional_arguments_count):
97+
raise exceptions.ArityError(
98+
len(signature), len(args), function_name)
99+
elif len(args) != required_arguments_count:
91100
raise exceptions.ArityError(
92101
len(signature), len(args), function_name)
93102
return self._type_check(args, signature, function_name)
94103

95104
def _type_check(self, actual, signature, function_name):
96-
for i in range(len(signature)):
97-
allowed_types = signature[i]['types']
105+
for i in range(min(len(signature), len(actual))):
106+
allowed_types = self._get_allowed_types_from_signature(signature[i])
98107
if allowed_types:
99108
self._type_check_single(actual[i], allowed_types,
100109
function_name)
101-
102110
def _type_check_single(self, current, types, function_name):
103111
# Type checking involves checking the top level type,
104112
# and in the case of arrays, potentially checking the types
@@ -122,6 +130,13 @@ def _type_check_single(self, current, types, function_name):
122130
self._subtype_check(current, allowed_subtypes,
123131
types, function_name)
124132

133+
## signature supports monotype {'type': 'type-name'}
134+
## or multiple types {'types': ['type1-name', 'type2-name']}
135+
def _get_allowed_types_from_signature(self, spec):
136+
if spec.get('type'):
137+
spec.update({'types': [spec.get('type')]})
138+
return spec.get('types')
139+
125140
def _get_allowed_pytypes(self, types):
126141
allowed_types = []
127142
allowed_subtypes = []
@@ -166,6 +181,14 @@ def _subtype_check(self, current, allowed_subtypes, types, function_name):
166181
@signature({'types': ['number']})
167182
def _func_abs(self, arg):
168183
return abs(arg)
184+
185+
@signature({'types': ['string']})
186+
def _func_lower(self, arg):
187+
return arg.lower()
188+
189+
@signature({'types': ['string']})
190+
def _func_upper(self, arg):
191+
return arg.upper()
169192

170193
@signature({'types': ['array-number']})
171194
def _func_avg(self, arg):
@@ -283,12 +306,172 @@ def _func_sort(self, arg):
283306
def _func_sum(self, arg):
284307
return sum(arg)
285308

309+
@signature({'types': ['object']})
310+
def _func_items(self, arg):
311+
return list(map(list, iteritems(arg)))
312+
313+
@signature({'types': ['array']})
314+
def _func_from_items(self, items):
315+
return dict(items)
316+
286317
@signature({"types": ['object']})
287318
def _func_keys(self, arg):
288319
# To be consistent with .values()
289320
# should we also return the indices of a list?
290321
return list(arg.keys())
291322

323+
@signature(
324+
{'type': 'string'},
325+
{'type': 'string'},
326+
{'type': 'number', 'optional': True},
327+
{'type': 'number', 'optional': True})
328+
def _func_find_first(self, text, search, start = 0, end = None):
329+
self._ensure_integer('find_first', 'start', start)
330+
self._ensure_integer('find_first', 'end', end)
331+
return self._find_impl(
332+
text,
333+
search,
334+
lambda t, s: t.find(s),
335+
start,
336+
end
337+
)
338+
339+
@signature(
340+
{'type': 'string'},
341+
{'type': 'string'},
342+
{'type': 'number', 'optional': True},
343+
{'type': 'number', 'optional': True})
344+
def _func_find_last(self, text, search, start = 0, end = None):
345+
self._ensure_integer('find_last', 'start', start)
346+
self._ensure_integer('find_last', 'end', end)
347+
return self._find_impl(
348+
text,
349+
search,
350+
lambda t, s: t.rfind(s),
351+
start,
352+
end
353+
)
354+
355+
def _find_impl(self, text, search, func, start, end):
356+
if len(search) == 0:
357+
return None
358+
if end == None:
359+
end = len(text)
360+
361+
pos = func(text[start:end], search)
362+
if start < 0:
363+
start = start + len(text)
364+
365+
# restrict resulting range to valid indices
366+
start = min(max(start, 0), len(text))
367+
return start + pos if pos != -1 else None
368+
369+
@signature(
370+
{'type': 'string'},
371+
{'type': 'number'},
372+
{'type': 'string', 'optional': True})
373+
def _func_pad_left(self, text, width, padding = ' '):
374+
self._ensure_non_negative_integer('pad_left', 'width', width)
375+
return self._pad_impl(lambda : text.rjust(width, padding), padding)
376+
377+
@signature(
378+
{'type': 'string'},
379+
{'type': 'number'},
380+
{'type': 'string', 'optional': True})
381+
def _func_pad_right(self, text, width, padding = ' '):
382+
self._ensure_non_negative_integer('pad_right', 'width', width)
383+
return self._pad_impl(lambda : text.ljust(width, padding), padding)
384+
385+
def _pad_impl(self, func, padding):
386+
if len(padding) != 1:
387+
raise exceptions.JMESPathError(
388+
'syntax-error: pad_right() expects $padding to have a '
389+
'single character, but received `{}` instead.'
390+
.format(padding))
391+
return func()
392+
393+
@signature(
394+
{'type': 'string'},
395+
{'type': 'string'},
396+
{'type': 'string'},
397+
{'type': 'number', 'optional': True})
398+
def _func_replace(self, text, search, replacement, count = None):
399+
self._ensure_non_negative_integer(
400+
'replace',
401+
'count',
402+
count)
403+
404+
if count != None:
405+
return text.replace(search, replacement, int(count))
406+
return text.replace(search, replacement)
407+
408+
@signature(
409+
{'type': 'string'},
410+
{'type': 'string'},
411+
{'type': 'number', 'optional': True})
412+
def _func_split(self, text, search, count = None):
413+
self._ensure_non_negative_integer(
414+
'split',
415+
'count',
416+
count)
417+
418+
if len(search) == 0:
419+
chars = list(text)
420+
if count == None:
421+
return chars
422+
423+
head = [c for c in chars[:count]]
424+
tail = [''.join(chars[count:])]
425+
return head + tail
426+
427+
if count != None:
428+
return text.split(search, count)
429+
return text.split(search)
430+
431+
def _ensure_integer(
432+
self,
433+
func_name,
434+
param_name,
435+
param_value):
436+
437+
if param_value != None:
438+
if int(param_value) != param_value:
439+
raise exceptions.JMESPathValueError(
440+
func_name,
441+
param_value,
442+
"integer")
443+
444+
def _ensure_non_negative_integer(
445+
self,
446+
func_name,
447+
param_name,
448+
param_value):
449+
450+
if param_value != None:
451+
if int(param_value) != param_value or int(param_value) < 0:
452+
raise exceptions.JMESPathValueError(
453+
func_name,
454+
param_name,
455+
"non-negative integer")
456+
457+
@signature({'type': 'string'}, {'type': 'string', 'optional': True})
458+
def _func_trim(self, text, chars = None):
459+
if chars == None or len(chars) == 0:
460+
return text.strip()
461+
return text.strip(chars)
462+
463+
@signature({'type': 'string'}, {'type': 'string', 'optional': True})
464+
def _func_trim_left(self, text, chars = None):
465+
if chars == None or len(chars) == 0:
466+
return text.lstrip()
467+
return text.lstrip(chars)
468+
469+
@signature({'type': 'string'}, {'type': 'string', 'optional': True})
470+
def _func_trim_right(self, text, chars = None):
471+
if chars == None or len(chars) == 0:
472+
return text.rstrip()
473+
return text.rstrip(chars)
474+
292475
@signature({"types": ['object']})
293476
def _func_values(self, arg):
294477
return list(arg.values())
@@ -348,6 +531,10 @@ def _func_max_by(self, array, expref):
348531
else:
349532
return None
350533

534+
@signature({'types': ['array'], 'variadic': True})
535+
def _func_zip(self, *arguments):
536+
return list(map(list, zip(*arguments)))
537+
351538
@signature({'types': ['array']}, {'types': ['expref']})
352539
def _func_group_by(self, array, expref):
353540
keyfunc = self._create_key_func(expref, ['null', 'string'], 'group_by')

jmespath/lexer.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ class Lexer(object):
2121
')': 'rparen',
2222
'{': 'lbrace',
2323
'}': 'rbrace',
24+
'+': 'plus',
25+
'%': 'modulo',
26+
u'\u2212': 'minus',
27+
u'\u00d7': 'multiply',
28+
u'\u00f7': 'divide',
2429
}
2530

2631
def tokenize(self, expression):
@@ -68,16 +73,30 @@ def tokenize(self, expression):
6873
yield {'type': 'number', 'value': int(buff),
6974
'start': start, 'end': start + len(buff)}
7075
elif self._current == '-':
71-
# Negative number.
72-
start = self._position
73-
buff = self._consume_number()
74-
if len(buff) > 1:
75-
yield {'type': 'number', 'value': int(buff),
76-
'start': start, 'end': start + len(buff)}
76+
if not self._peek_is_next_digit():
77+
self._next()
78+
yield {'type': 'minus', 'value': '-',
79+
'start': self._position - 1, 'end': self._position}
80+
else:
81+
# Negative number.
82+
start = self._position
83+
buff = self._consume_number()
84+
if len(buff) > 1:
85+
yield {'type': 'number', 'value': int(buff),
86+
'start': start, 'end': start + len(buff)}
87+
else:
88+
raise LexerError(lexer_position=start,
89+
lexer_value=buff,
90+
message="Unknown token '%s'" % buff)
91+
elif self._current == '/':
92+
self._next()
93+
if self._current == '/':
94+
self._next()
95+
yield {'type': 'div', 'value': '//',
96+
'start': self._position - 1, 'end': self._position}
7797
else:
78-
raise LexerError(lexer_position=start,
79-
lexer_value=buff,
80-
message="Unknown token '%s'" % buff)
98+
yield {'type': 'divide', 'value': '/',
99+
'start': self._position, 'end': self._position + 1}
81100
elif self._current == '"':
82101
yield self._consume_quoted_identifier()
83102
elif self._current == '<':
@@ -117,6 +136,13 @@ def _consume_number(self):
117136
buff += self._current
118137
return buff
119138

139+
def _peek_is_next_digit(self):
140+
if (self._position == self._length - 1):
141+
return False
142+
else:
143+
next = self._chars[self._position + 1]
144+
return next in self.VALID_NUMBER
145+
120146
def _initialize_for_expression(self, expression):
121147
if not expression:
122148
raise EmptyExpressionError()

0 commit comments

Comments
 (0)