Skip to content

Commit cf10db6

Browse files
committed
Implement various error message improvements.
Closes #39.
1 parent 436e13f commit cf10db6

File tree

9 files changed

+203
-86
lines changed

9 files changed

+203
-86
lines changed

graphql/core/execution/values.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ def get_variable_value(schema, definition_ast, input):
7979
[definition_ast]
8080
)
8181

82-
if is_valid_value(type, input):
82+
errors = is_valid_value(input, type)
83+
if not errors:
8384
if input is None:
8485
default_value = definition_ast.default_value
8586
if default_value:
@@ -96,11 +97,12 @@ def get_variable_value(schema, definition_ast, input):
9697
[definition_ast]
9798
)
9899

100+
message = (u'\n' + u'\n'.join(errors)) if errors else u''
99101
raise GraphQLError(
100-
'Variable "${}" expected value of type "{}" but got: {}.'.format(
102+
'Variable "${}" got invalid value {}.{}'.format(
101103
variable.name.value,
102-
print_ast(definition_ast.type),
103-
json.dumps(input, sort_keys=True)
104+
json.dumps(input, sort_keys=True),
105+
message
104106
),
105107
[definition_ast]
106108
)
Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from ..language import ast
2+
from ..language.printer import print_ast
23
from ..type.definition import (
34
GraphQLEnumType,
45
GraphQLInputObjectType,
@@ -7,45 +8,66 @@
78
GraphQLScalarType,
89
)
910

11+
_empty_list = []
12+
1013

1114
def is_valid_literal_value(type, value_ast):
1215
if isinstance(type, GraphQLNonNull):
16+
of_type = type.of_type
1317
if not value_ast:
14-
return False
18+
if of_type.name:
19+
return [u'Expected "{}", found null.'.format(type)]
20+
21+
return [u'Expected non-null value, found null.']
1522

16-
of_type = type.of_type
1723
return is_valid_literal_value(of_type, value_ast)
1824

1925
if not value_ast:
20-
return True
26+
return _empty_list
2127

2228
if isinstance(value_ast, ast.Variable):
23-
return True
29+
return _empty_list
2430

2531
if isinstance(type, GraphQLList):
2632
item_type = type.of_type
2733
if isinstance(value_ast, ast.ListValue):
28-
return all(is_valid_literal_value(item_type, item_ast) for item_ast in value_ast.values)
34+
errors = []
35+
36+
for i, item_ast in enumerate(value_ast.values):
37+
item_errors = is_valid_literal_value(item_type, item_ast)
38+
for error in item_errors:
39+
errors.append(u'In element #{}: {}'.format(i, error))
40+
41+
return errors
2942

3043
return is_valid_literal_value(item_type, value_ast)
3144

3245
if isinstance(type, GraphQLInputObjectType):
3346
if not isinstance(value_ast, ast.ObjectValue):
34-
return False
47+
return [u'Expected "{}", found not an object.'.format(type)]
3548

3649
fields = type.get_fields()
3750
field_asts = value_ast.fields
3851

39-
if any(not fields.get(field_ast.name.value, None) for field_ast in field_asts):
40-
return False
52+
errors = []
53+
for provided_field_ast in field_asts:
54+
if provided_field_ast.name.value not in fields:
55+
errors.append(u'In field "{}": Unknown field.'.format(provided_field_ast.name.value))
4156

4257
field_ast_map = {field_ast.name.value: field_ast for field_ast in field_asts}
43-
get_field_ast_value = lambda field_name: field_ast_map[
44-
field_name].value if field_name in field_ast_map else None
58+
get_field_ast_value = lambda field_name: field_ast_map[field_name].value \
59+
if field_name in field_ast_map else None
4560

46-
return all(is_valid_literal_value(field.type, get_field_ast_value(field_name))
47-
for field_name, field in fields.items())
61+
for field_name, field in fields.items():
62+
subfield_errors = is_valid_literal_value(field.type, get_field_ast_value(field_name))
63+
errors.extend(u'In field "{}": {}'.format(field_name, e) for e in subfield_errors)
64+
65+
return errors
4866

4967
assert isinstance(type, (GraphQLScalarType, GraphQLEnumType)), 'Must be input type'
5068

51-
return type.parse_literal(value_ast) is not None
69+
parse_result = type.parse_literal(value_ast)
70+
if parse_result is None:
71+
return [u'Expected type "{}", found {}.'.format(type.name, print_ast(value_ast))]
72+
73+
return _empty_list

graphql/core/utils/is_valid_value.py

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import collections
6+
import json
67
from six import string_types
78
from ..type import (
89
GraphQLEnumType,
@@ -12,45 +13,62 @@
1213
GraphQLScalarType,
1314
)
1415

16+
_empty_list = []
1517

16-
def is_valid_value(type, value):
18+
19+
def is_valid_value(value, type):
1720
"""Given a type and any value, return True if that value is valid."""
1821
if isinstance(type, GraphQLNonNull):
22+
of_type = type.of_type
1923
if value is None:
20-
return False
24+
if hasattr(of_type, 'name'):
25+
return [u'Expected "{}", found null.'.format(type)]
26+
27+
return [u'Expected non-null value, found null.']
2128

22-
return is_valid_value(type.of_type, value)
29+
return is_valid_value(value, of_type)
2330

2431
if value is None:
25-
return True
32+
return _empty_list
2633

2734
if isinstance(type, GraphQLList):
2835
item_type = type.of_type
29-
if not isinstance(value, string_types) and \
30-
isinstance(value, collections.Iterable):
31-
return all(is_valid_value(item_type, item) for item in value)
36+
if not isinstance(value, string_types) and isinstance(value, collections.Iterable):
37+
errors = []
38+
for i, item in enumerate(value):
39+
item_errors = is_valid_value(item, item_type)
40+
for error in item_errors:
41+
errors.append(u'In element #{}: {}'.format(i, error))
42+
43+
return errors
44+
3245
else:
33-
return is_valid_value(item_type, value)
46+
return is_valid_value(value, item_type)
3447

3548
if isinstance(type, GraphQLInputObjectType):
3649
if not isinstance(value, collections.Mapping):
37-
return False
50+
return [u'Expected "{}", found not an object.'.format(type)]
3851

3952
fields = type.get_fields()
53+
errors = []
54+
55+
for provided_field in value.keys():
56+
if provided_field not in fields:
57+
errors.append(u'In field "{}": Unknown field.'.format(provided_field))
4058

41-
# Ensure every provided field is defined.
42-
if any(field_name not in fields for field_name in value.keys()):
43-
return False
59+
for field_name, field in fields.items():
60+
subfield_errors = is_valid_value(value.get(field_name), field.type)
61+
errors.extend(u'In field "{}": {}'.format(field_name, e) for e in subfield_errors)
4462

45-
# Ensure every defined field is valid.
46-
return all(
47-
is_valid_value(field.type, value.get(field_name))
48-
for field_name, field in fields.items()
49-
)
63+
return errors
5064

5165
assert isinstance(type, (GraphQLScalarType, GraphQLEnumType)), \
5266
'Must be input type'
5367

5468
# Scalar/Enum input checks to ensure the type can parse the value to
5569
# a non-null value.
56-
return type.parse_value(value) is not None
70+
parse_result = type.parse_value(value)
71+
if parse_result is None:
72+
return [u'Expected type "{}", found {}.'.format(type, json.dumps(value))]
73+
74+
return _empty_list

graphql/core/validation/rules/arguments_of_correct_type.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
class ArgumentsOfCorrectType(ValidationRule):
88
def enter_Argument(self, node, key, parent, path, ancestors):
99
arg_def = self.context.get_argument()
10-
if arg_def and not is_valid_literal_value(arg_def.type, node.value):
11-
return GraphQLError(
12-
self.bad_value_message(node.name.value, arg_def.type,
13-
print_ast(node.value)),
14-
[node.value]
15-
)
10+
if arg_def:
11+
errors = is_valid_literal_value(arg_def.type, node.value)
12+
if errors:
13+
return GraphQLError(
14+
self.bad_value_message(node.name.value, arg_def.type,
15+
print_ast(node.value), errors),
16+
[node.value]
17+
)
1618

1719
@staticmethod
18-
def bad_value_message(arg_name, type, value):
19-
return 'Argument "{}" expected type "{}" but got: {}.'.format(arg_name, type, value)
20+
def bad_value_message(arg_name, type, value, verbose_errors):
21+
message = (u'\n' + u'\n'.join(verbose_errors)) if verbose_errors else ''
22+
return 'Argument "{}" has invalid value {}.{}'.format(arg_name, value, message)

graphql/core/validation/rules/default_values_of_correct_type.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@ def enter_VariableDefinition(self, node, key, parent, path, ancestors):
1717
[default_value]
1818
)
1919

20-
if type and default_value and not is_valid_literal_value(type, default_value):
21-
return GraphQLError(
22-
self.bad_value_for_default_arg_message(name, type, print_ast(default_value)),
23-
[default_value]
24-
)
20+
if type and default_value:
21+
errors = is_valid_literal_value(type, default_value)
22+
if errors:
23+
return GraphQLError(
24+
self.bad_value_for_default_arg_message(name, type, print_ast(default_value), errors),
25+
[default_value]
26+
)
2527

2628
@staticmethod
2729
def default_for_non_null_arg_message(var_name, type, guess_type):
28-
return 'Variable "${}" of type "{}" is required and will not use the default value. ' \
29-
'Perhaps you meant to use type "{}".'.format(var_name, type, guess_type)
30+
return u'Variable "${}" of type "{}" is required and will not use the default value. ' \
31+
u'Perhaps you meant to use type "{}".'.format(var_name, type, guess_type)
3032

3133
@staticmethod
32-
def bad_value_for_default_arg_message(var_name, type, value):
33-
return 'Variable "${}" of type "{}" has invalid default value: {}.'.format(var_name, type, value)
34+
def bad_value_for_default_arg_message(var_name, type, value, verbose_errors):
35+
message = (u'\n' + u'\n'.join(verbose_errors)) if verbose_errors else u''
36+
return u'Variable "${}" of type "{}" has invalid default value: {}.{}'.format(var_name, type, value, message)

tests/core_execution/test_variables.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ def input_to_json(obj, args, info):
3939
return stringify(input)
4040

4141

42+
TestNestedInputObject = GraphQLInputObjectType(
43+
name='TestNestedInputObject',
44+
fields={
45+
'na': GraphQLInputObjectField(GraphQLNonNull(TestInputObject)),
46+
'nb': GraphQLInputObjectField(GraphQLNonNull(GraphQLString))
47+
}
48+
)
49+
4250
TestType = GraphQLObjectType('TestType', {
4351
'fieldWithObjectInput': GraphQLField(
4452
GraphQLString,
@@ -56,6 +64,10 @@ def input_to_json(obj, args, info):
5664
GraphQLString,
5765
args={'input': GraphQLArgument(GraphQLString, 'Hello World')},
5866
resolver=input_to_json),
67+
'fieldWithNestedInputObject': GraphQLField(
68+
GraphQLString,
69+
args={'input': GraphQLArgument(TestNestedInputObject, 'Hello World')},
70+
resolver=input_to_json),
5971
'list': GraphQLField(
6072
GraphQLString,
6173
args={'input': GraphQLArgument(GraphQLList(GraphQLString))},
@@ -135,6 +147,7 @@ def test_does_not_use_incorrect_value():
135147
})
136148

137149

150+
# noinspection PyMethodMayBeStatic
138151
class TestUsingVariables:
139152
doc = '''
140153
query q($input: TestInputObject) {
@@ -179,8 +192,8 @@ def test_errors_on_null_for_nested_non_null(self):
179192

180193
assert format_error(excinfo.value) == {
181194
'locations': [{'column': 13, 'line': 2}],
182-
'message': 'Variable "$input" expected value of type "TestInputObject" but got: '
183-
'{}.'.format(stringify(params['input']))
195+
'message': 'Variable "$input" got invalid value {}.\n'
196+
'In field "c": Expected "String!", found null.'.format(stringify(params['input']))
184197
}
185198

186199
def test_errors_on_incorrect_type(self):
@@ -191,8 +204,8 @@ def test_errors_on_incorrect_type(self):
191204

192205
assert format_error(excinfo.value) == {
193206
'locations': [{'column': 13, 'line': 2}],
194-
'message': 'Variable "$input" expected value of type "TestInputObject" but got: '
195-
'{}.'.format(stringify(params['input']))
207+
'message': 'Variable "$input" got invalid value {}.\n'
208+
'Expected "TestInputObject", found not an object.'.format(stringify(params['input']))
196209
}
197210

198211
def test_errors_on_omission_of_nested_non_null(self):
@@ -203,20 +216,50 @@ def test_errors_on_omission_of_nested_non_null(self):
203216

204217
assert format_error(excinfo.value) == {
205218
'locations': [{'column': 13, 'line': 2}],
206-
'message': 'Variable "$input" expected value of type "TestInputObject" but got: '
207-
'{}.'.format(stringify(params['input']))
219+
'message': 'Variable "$input" got invalid value {}.\n'
220+
'In field "c": Expected "String!", found null.'.format(stringify(params['input']))
208221
}
209222

210-
def test_errors_on_addition_of_unknown_input_field(self):
223+
def test_errors_on_deep_nested_errors_and_with_many_errors(self):
224+
nested_doc = '''
225+
query q($input: TestNestedInputObject) {
226+
fieldWithNestedObjectInput(input: $input)
227+
}
228+
'''
229+
230+
params = {'input': {'na': {'a': 'foo'}}}
231+
with raises(GraphQLError) as excinfo:
232+
check(nested_doc, {}, params)
233+
234+
assert format_error(excinfo.value) == {
235+
'locations': [{'column': 19, 'line': 2}],
236+
'message': 'Variable "$input" got invalid value {}.\n'
237+
'In field "na": In field "c": Expected "String!", found null.\n'
238+
'In field "nb": Expected "String!", found null.'.format(stringify(params['input']))
239+
}
240+
241+
def test_errors_on_addition_of_input_field_of_incorrect_type(self):
211242
params = {'input': {'a': 'foo', 'b': 'bar', 'c': 'baz', 'd': 'dog'}}
212243

213244
with raises(GraphQLError) as excinfo:
214245
check(self.doc, {}, params)
215246

216247
assert format_error(excinfo.value) == {
217248
'locations': [{'column': 13, 'line': 2}],
218-
'message': 'Variable "$input" expected value of type "TestInputObject" but got: '
219-
'{}.'.format(stringify(params['input']))
249+
'message': 'Variable "$input" got invalid value {}.\n'
250+
'In field "d": Expected type "ComplexScalar", found "dog".'.format(stringify(params['input']))
251+
}
252+
253+
def test_errors_on_addition_of_unknown_input_field(self):
254+
params = {'input': {'a': 'foo', 'b': 'bar', 'c': 'baz', 'f': 'dog'}}
255+
256+
with raises(GraphQLError) as excinfo:
257+
check(self.doc, {}, params)
258+
259+
assert format_error(excinfo.value) == {
260+
'locations': [{'column': 13, 'line': 2}],
261+
'message': 'Variable "$input" got invalid value {}.\n'
262+
'In field "f": Unknown field.'.format(stringify(params['input']))
220263
}
221264

222265

@@ -496,8 +539,8 @@ def test_does_not_allow_lists_of_non_nulls_to_contain_null():
496539

497540
assert format_error(excinfo.value) == {
498541
'locations': [{'column': 13, 'line': 2}],
499-
'message': 'Variable "$input" expected value of type "[String!]" but got: '
500-
'{}.'.format(stringify(params['input']))
542+
'message': 'Variable "$input" got invalid value {}.\n'
543+
'In element #1: Expected "String!", found null.'.format(stringify(params['input']))
501544
}
502545

503546

@@ -544,8 +587,8 @@ def test_does_not_allow_non_null_lists_of_non_nulls_to_contain_null():
544587

545588
assert format_error(excinfo.value) == {
546589
'locations': [{'column': 13, 'line': 2}],
547-
'message': 'Variable "$input" expected value of type "[String!]!" but got: '
548-
'{}.'.format(stringify(params['input']))
590+
'message': 'Variable "$input" got invalid value {}.\n'
591+
'In element #1: Expected "String!", found null.'.format(stringify(params['input']))
549592
}
550593

551594

0 commit comments

Comments
 (0)