Skip to content

Commit 8db8bae

Browse files
authored
Merge branch 'develop' into jep/lexical-scoping
2 parents e21ac5c + 827ac0d commit 8db8bae

18 files changed

+914
-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: 208 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
import inspect
44
from pydoc import resolve
55

6+
from collections import OrderedDict
7+
68
from jmespath import exceptions
7-
from jmespath.compat import string_type as STRING_TYPE
89
from jmespath.compat import get_methods
10+
from jmespath.compat import iteritems
11+
from jmespath.compat import map
12+
from jmespath.compat import string_type as STRING_TYPE
913

1014

1115
# python types -> jmespath types
@@ -90,22 +94,28 @@ def call_function(self, function_name, resolved_args, *args, **kwargs):
9094
return function(self, *resolved_args)
9195

9296
def _validate_arguments(self, args, signature, function_name):
93-
if signature and signature[-1].get('variadic'):
97+
required_arguments_count = len([param for param in signature if not param.get('optional') or not param['optional']])
98+
optional_arguments_count = len([param for param in signature if param.get('optional') and param['optional']])
99+
has_variadic = signature[-1].get('variadic') if signature != None else False
100+
if has_variadic:
94101
if len(args) < len(signature):
95102
raise exceptions.VariadictArityError(
96103
len(signature), len(args), function_name)
97-
elif len(args) != len(signature):
104+
elif optional_arguments_count > 0:
105+
if len(args) < required_arguments_count or len(args) > (required_arguments_count + optional_arguments_count):
106+
raise exceptions.ArityError(
107+
len(signature), len(args), function_name)
108+
elif len(args) != required_arguments_count:
98109
raise exceptions.ArityError(
99110
len(signature), len(args), function_name)
100111
return self._type_check(args, signature, function_name)
101112

102113
def _type_check(self, actual, signature, function_name):
103-
for i in range(len(signature)):
104-
allowed_types = signature[i]['types']
114+
for i in range(min(len(signature), len(actual))):
115+
allowed_types = self._get_allowed_types_from_signature(signature[i])
105116
if allowed_types:
106117
self._type_check_single(actual[i], allowed_types,
107118
function_name)
108-
109119
def _type_check_single(self, current, types, function_name):
110120
# Type checking involves checking the top level type,
111121
# and in the case of arrays, potentially checking the types
@@ -129,6 +139,13 @@ def _type_check_single(self, current, types, function_name):
129139
self._subtype_check(current, allowed_subtypes,
130140
types, function_name)
131141

142+
## signature supports monotype {'type': 'type-name'}
143+
## or multiple types {'types': ['type1-name', 'type2-name']}
144+
def _get_allowed_types_from_signature(self, spec):
145+
if spec.get('type'):
146+
spec.update({'types': [spec.get('type')]})
147+
return spec.get('types')
148+
132149
def _get_allowed_pytypes(self, types):
133150
allowed_types = []
134151
allowed_subtypes = []
@@ -173,6 +190,14 @@ def _subtype_check(self, current, allowed_subtypes, types, function_name):
173190
@signature({'types': ['number']})
174191
def _func_abs(self, arg):
175192
return abs(arg)
193+
194+
@signature({'types': ['string']})
195+
def _func_lower(self, arg):
196+
return arg.lower()
197+
198+
@signature({'types': ['string']})
199+
def _func_upper(self, arg):
200+
return arg.upper()
176201

177202
@signature({'types': ['array-number']})
178203
def _func_avg(self, arg):
@@ -290,12 +315,172 @@ def _func_sort(self, arg):
290315
def _func_sum(self, arg):
291316
return sum(arg)
292317

318+
@signature({'types': ['object']})
319+
def _func_items(self, arg):
320+
return list(map(list, iteritems(arg)))
321+
322+
@signature({'types': ['array']})
323+
def _func_from_items(self, items):
324+
return dict(items)
325+
293326
@signature({"types": ['object']})
294327
def _func_keys(self, arg):
295328
# To be consistent with .values()
296329
# should we also return the indices of a list?
297330
return list(arg.keys())
298331

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

549+
@signature({'types': ['array'], 'variadic': True})
550+
def _func_zip(self, *arguments):
551+
return list(map(list, zip(*arguments)))
552+
553+
@signature({'types': ['array']}, {'types': ['expref']})
554+
def _func_group_by(self, array, expref):
555+
keyfunc = self._create_key_func(expref, ['null', 'string'], 'group_by')
556+
if array:
557+
result = OrderedDict()
558+
keys = list(dict.fromkeys([keyfunc(item) for item in array if keyfunc(item) != None]))
559+
for key in keys:
560+
items = [ item for item in array if keyfunc(item) == key ]
561+
result.update({key: items})
562+
return result
563+
else:
564+
return None
565+
364566
def _create_key_func(self, expref, allowed_types, function_name):
365567
def keyfunc(x):
366568
result = expref.visit(expref.expression, x)

0 commit comments

Comments
 (0)