Skip to content

Commit 03a1461

Browse files
committed
Merge remote-tracking branch 'jamesls/fix-error-messages' into develop
* jamesls/fix-error-messages: Remove redundant error check in jp Raise IncompleteExpressionError where appropriate Use singular argument when expected arity is 1 Add test for empty array with sort_by Fix invalid-type error messages
2 parents 5726355 + 047ca77 commit 03a1461

File tree

7 files changed

+83
-21
lines changed

7 files changed

+83
-21
lines changed

bin/jp

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,6 @@ def main():
5151
except exceptions.UnknownFunctionError as e:
5252
sys.stderr.write("unknown-function: %s\n" % e)
5353
return 1
54-
except exceptions.LexerError as e:
55-
sys.stderr.write("syntax-error: %s\n" % e)
56-
return 1
5754
except exceptions.ParseError as e:
5855
sys.stderr.write("syntax-error: %s\n" % e)
5956
return 1

jmespath/exceptions.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ def __str__(self):
2222
# self.lex_position +1 to account for the starting double quote char.
2323
underline = ' ' * (self.lex_position + 1) + '^'
2424
return (
25-
'%s: Parse error at column %s near '
26-
'token "%s" (%s) for expression:\n"%s"\n%s' % (
25+
'%s: Parse error at column %s, '
26+
'token "%s" (%s), for expression:\n"%s"\n%s' % (
2727
self.msg, self.lex_position, self.token_value, self.token_type,
2828
self.expression, underline))
2929

@@ -71,19 +71,29 @@ def __init__(self, expected, actual, name):
7171
self.expression = None
7272

7373
def __str__(self):
74-
return ("Expected %s arguments for function %s(), "
75-
"received %s" % (self.expected_arity,
76-
self.function_name,
77-
self.actual_arity))
74+
return ("Expected %s %s for function %s(), "
75+
"received %s" % (
76+
self.expected_arity,
77+
self._pluralize('argument', self.expected_arity),
78+
self.function_name,
79+
self.actual_arity))
80+
81+
def _pluralize(self, word, count):
82+
if count == 1:
83+
return word
84+
else:
85+
return word + 's'
7886

7987

8088
@with_str_method
8189
class VariadictArityError(ArityError):
8290
def __str__(self):
83-
return ("Expected at least %s arguments for function %s, "
84-
"received %s" % (self.expected_arity,
85-
self.function_name,
86-
self.actual_arity))
91+
return ("Expected at least %s %s for function %s(), "
92+
"received %s" % (
93+
self.expected_arity,
94+
self._pluralize('argument', self.expected_arity),
95+
self.function_name,
96+
self.actual_arity))
8797

8898

8999
@with_str_method

jmespath/functions.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ def _func_sort_by(self, array, expref):
317317
# that validates that type, which requires that remaining array
318318
# elements resolve to the same type as the first element.
319319
required_type = self._convert_to_jmespath_type(
320-
self.interpreter.visit(expref.expression, array[0]))
320+
type(self.interpreter.visit(expref.expression, array[0])).__name__)
321321
if required_type not in ['number', 'string']:
322322
raise exceptions.JMESPathTypeError(
323323
'sort_by', array[0], required_type, ['string', 'number'])
@@ -345,12 +345,14 @@ def _create_key_func(self, expr_node, allowed_types, function_name):
345345

346346
def keyfunc(x):
347347
result = interpreter.visit(expr_node, x)
348-
jmespath_type = self._convert_to_jmespath_type(result)
348+
actual_typename = type(result).__name__
349+
jmespath_type = self._convert_to_jmespath_type(actual_typename)
350+
# allowed_types is in term of jmespath types, not python types.
349351
if jmespath_type not in allowed_types:
350352
raise exceptions.JMESPathTypeError(
351353
function_name, result, jmespath_type, allowed_types)
352354
return result
353355
return keyfunc
354356

355357
def _convert_to_jmespath_type(self, pyobject):
356-
return TYPES_MAP.get(type(pyobject).__name__, 'unknown')
358+
return TYPES_MAP.get(pyobject, 'unknown')

jmespath/parser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,9 @@ def _assert_not_token(self, *token_types):
427427
"Token %s not allowed to be: %s" % (actual_type, token_types))
428428

429429
def _error_nud_token(self, token):
430+
if token['type'] == 'eof':
431+
raise exceptions.IncompleteExpressionError(
432+
token['start'], token['value'], token['type'])
430433
raise exceptions.ParseError(token['start'], token['value'],
431434
token['type'], 'Invalid token.')
432435

tests/compliance/functions.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@
609609
},
610610
"cases": [
611611
{
612-
"description": "function projection on variadic function",
612+
"description": "sort by field expression",
613613
"expression": "sort_by(people, &age)",
614614
"result": [
615615
{"age": 10, "age_str": "10", "bool": true, "name": 3},
@@ -620,7 +620,7 @@
620620
]
621621
},
622622
{
623-
"description": "function projection on variadic function",
623+
"description": "sort by function expression",
624624
"expression": "sort_by(people, &to_number(age_str))",
625625
"result": [
626626
{"age": 10, "age_str": "10", "bool": true, "name": 3},
@@ -631,7 +631,7 @@
631631
]
632632
},
633633
{
634-
"description": "function projection on variadic function",
634+
"description": "function projection on sort_by function",
635635
"expression": "sort_by(people, &age)[].name",
636636
"result": [3, "a", "c", "b", "d"]
637637
},
@@ -655,6 +655,10 @@
655655
"expression": "sort_by(people, &age)[].extra",
656656
"result": ["foo", "bar"]
657657
},
658+
{
659+
"expression": "sort_by(`[]`, &age)",
660+
"result": []
661+
},
658662
{
659663
"expression": "max_by(people, &age)",
660664
"result": {"age": 50, "age_str": "50", "bool": false, "name": "d"}

tests/test_functions.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55

66
import jmespath
7+
from jmespath import exceptions
78

89

910
class TestFunctions(unittest.TestCase):
@@ -16,3 +17,41 @@ def test_can_max_datetimes(self):
1617
data = [datetime.now(), datetime.now() + timedelta(seconds=1)]
1718
result = jmespath.search('max([*].to_string(@))', data)
1819
self.assertEqual(json.loads(result), str(data[-1]))
20+
21+
def test_type_error_messages(self):
22+
with self.assertRaises(exceptions.JMESPathTypeError) as e:
23+
jmespath.search('length(@)', 2)
24+
exception = e.exception
25+
# 1. Function name should be in error message
26+
self.assertIn('length()', str(exception))
27+
# 2. Mention it's an invalid type
28+
self.assertIn('invalid type for value: 2', str(exception))
29+
# 3. Mention the valid types:
30+
self.assertIn("expected one of: ['string', 'array', 'object']",
31+
str(exception))
32+
# 4. Mention the actual type.
33+
self.assertIn('received: "number"', str(exception))
34+
35+
def test_singular_in_error_message(self):
36+
with self.assertRaises(exceptions.ArityError) as e:
37+
jmespath.search('length(@, @)', [0, 1])
38+
exception = e.exception
39+
self.assertEqual(
40+
str(exception),
41+
'Expected 1 argument for function length(), received 2')
42+
43+
def test_error_message_is_pluralized(self):
44+
with self.assertRaises(exceptions.ArityError) as e:
45+
jmespath.search('sort_by(@)', [0, 1])
46+
exception = e.exception
47+
self.assertEqual(
48+
str(exception),
49+
'Expected 2 arguments for function sort_by(), received 1')
50+
51+
def test_variadic_is_pluralized(self):
52+
with self.assertRaises(exceptions.VariadictArityError) as e:
53+
jmespath.search('not_null()', 'foo')
54+
exception = e.exception
55+
self.assertEqual(
56+
str(exception),
57+
'Expected at least 1 argument for function not_null(), received 0')

tests/test_parser.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ def test_bad_parse(self):
121121

122122
def test_bad_parse_error_message(self):
123123
error_message = (
124-
'Unexpected token: ]: Parse error at column 3 '
125-
'near token "]" (RBRACKET) for expression:\n'
124+
'Unexpected token: ]: Parse error at column 3, '
125+
'token "]" (RBRACKET), for expression:\n'
126126
'"foo]baz"\n'
127127
' ^')
128128
self.assert_error_message('foo]baz', error_message)
@@ -134,6 +134,13 @@ def test_bad_parse_error_message_with_multiselect(self):
134134
' ^')
135135
self.assert_error_message('foo.{bar: baz,bar: bar', error_message)
136136

137+
def test_incomplete_expression_with_missing_paren(self):
138+
error_message = (
139+
'Invalid jmespath expression: Incomplete expression:\n'
140+
'"length(@,"\n'
141+
' ^')
142+
self.assert_error_message('length(@,', error_message)
143+
137144
def test_bad_lexer_values(self):
138145
error_message = (
139146
'Bad jmespath expression: '

0 commit comments

Comments
 (0)