Skip to content

Commit 8e20b27

Browse files
committed
Merge pull request #52 from graphql-python/releases/0.4.15
Way to 0.4.15
2 parents d540fdf + 421db50 commit 8e20b27

File tree

11 files changed

+153
-45
lines changed

11 files changed

+153
-45
lines changed

graphql/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'''
2+
GraphQL provides a Python implementation for the GraphQL specification
3+
but is also a useful utility for operating on GraphQL files and building
4+
sophisticated tools.
5+
6+
This primary module exports a general purpose function for fulfilling all
7+
steps of the GraphQL specification in a single operation, but also includes
8+
utilities for every part of the GraphQL specification:
9+
10+
- Parsing the GraphQL language.
11+
- Building a GraphQL type schema.
12+
- Validating a GraphQL request against a type schema.
13+
- Executing a GraphQL request against a type schema.
14+
15+
This also includes utility functions for operating on GraphQL types and
16+
GraphQL documents to facilitate building tools.
17+
'''

graphql/core/execution/executor.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,8 @@ def complete_value(self, ctx, return_type, field_asts, info, result):
252252
# If field type is List, complete each item in the list with the inner type
253253
if isinstance(return_type, GraphQLList):
254254
assert isinstance(result, collections.Iterable), \
255-
'User Error: expected iterable, but did not find one.'
255+
('User Error: expected iterable, but did not find one' +
256+
'for field {}.{}').format(info.parent_type, info.field_name)
256257

257258
item_type = return_type.of_type
258259
completed_results = []

graphql/core/language/parser.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,10 @@ def parse_definition(parser):
209209
return parse_operation_definition(parser)
210210
elif name == 'fragment':
211211
return parse_fragment_definition(parser)
212-
elif name in ('type', 'interface', 'union', 'scalar', 'enum', 'input', 'extend'):
212+
elif name in ('type', 'interface', 'union', 'scalar', 'enum', 'input'):
213213
return parse_type_definition(parser)
214+
elif name == 'extend':
215+
return parse_type_extension_definition(parser)
214216

215217
raise unexpected(parser)
216218

@@ -530,9 +532,6 @@ def parse_type_definition(parser):
530532
elif name == 'input':
531533
return parse_input_object_type_definition(parser)
532534

533-
elif name == 'extend':
534-
return parse_type_extension_definition(parser)
535-
536535
raise unexpected(parser)
537536

538537

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1+
from collections import Counter, OrderedDict
2+
try:
3+
# Python 2
4+
from itertools import izip
5+
except ImportError:
6+
# Python 3
7+
izip = zip
8+
19
from ...error import GraphQLError
10+
from ...type.definition import GraphQLObjectType, is_abstract_type
211
from .base import ValidationRule
312

413

14+
class OrderedCounter(Counter, OrderedDict):
15+
pass
16+
17+
518
class FieldsOnCorrectType(ValidationRule):
619

720
def enter_Field(self, node, key, parent, path, ancestors):
@@ -11,11 +24,49 @@ def enter_Field(self, node, key, parent, path, ancestors):
1124

1225
field_def = self.context.get_field_def()
1326
if not field_def:
27+
suggested_types = []
28+
if is_abstract_type(type):
29+
suggested_types = get_sibling_interfaces_including_field(type, node.name.value)
30+
suggested_types += get_implementations_including_field(type, node.name.value)
1431
self.context.report_error(GraphQLError(
15-
self.undefined_field_message(node.name.value, type.name),
32+
self.undefined_field_message(node.name.value, type.name, suggested_types),
1633
[node]
1734
))
1835

1936
@staticmethod
20-
def undefined_field_message(field_name, type):
21-
return 'Cannot query field "{}" on "{}".'.format(field_name, type)
37+
def undefined_field_message(field_name, type, suggested_types):
38+
message = 'Cannot query field "{}" on type "{}".'.format(field_name, type)
39+
MAX_LENGTH = 5
40+
if suggested_types:
41+
suggestions = ', '.join(['"{}"'.format(t) for t in suggested_types[:MAX_LENGTH]])
42+
l_suggested_types = len(suggested_types)
43+
if l_suggested_types > MAX_LENGTH:
44+
suggestions += ", and {} other types".format(l_suggested_types-MAX_LENGTH)
45+
message += " However, this field exists on {}.".format(suggestions)
46+
message += " Perhaps you meant to use an inline fragment?"
47+
return message
48+
49+
50+
def get_implementations_including_field(type, field_name):
51+
'''Return implementations of `type` that include `fieldName` as a valid field.'''
52+
return sorted(map(lambda t: t.name, filter(lambda t: field_name in t.get_fields(), type.get_possible_types())))
53+
54+
55+
def get_sibling_interfaces_including_field(type, field_name):
56+
'''Go through all of the implementations of type, and find other interaces
57+
that they implement. If those interfaces include `field` as a valid field,
58+
return them, sorted by how often the implementations include the other
59+
interface.'''
60+
61+
implementing_objects = filter(lambda t: isinstance(t, GraphQLObjectType), type.get_possible_types())
62+
suggested_interfaces = OrderedCounter()
63+
for t in implementing_objects:
64+
for i in t.get_interfaces():
65+
if field_name not in i.get_fields():
66+
continue
67+
suggested_interfaces[i.name] += 1
68+
most_common = suggested_interfaces.most_common()
69+
if not most_common:
70+
return []
71+
# Get first element of each list (excluding the counter int)
72+
return list(next(izip(*most_common)))

graphql/core/validation/rules/no_unused_variables.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,23 @@ def enter_OperationDefinition(self, node, key, parent, path, ancestors):
1515
def leave_OperationDefinition(self, operation, key, parent, path, ancestors):
1616
variable_name_used = set()
1717
usages = self.context.get_recursive_variable_usages(operation)
18+
op_name = operation.name and operation.name.value or None
1819

1920
for variable_usage in usages:
2021
variable_name_used.add(variable_usage.node.name.value)
2122

2223
for variable_definition in self.variable_definitions:
2324
if variable_definition.variable.name.value not in variable_name_used:
2425
self.context.report_error(GraphQLError(
25-
self.unused_variable_message(variable_definition.variable.name.value),
26+
self.unused_variable_message(variable_definition.variable.name.value, op_name),
2627
[variable_definition]
2728
))
2829

2930
def enter_VariableDefinition(self, node, key, parent, path, ancestors):
3031
self.variable_definitions.append(node)
3132

3233
@staticmethod
33-
def unused_variable_message(variable_name):
34+
def unused_variable_message(variable_name, op_name):
35+
if op_name:
36+
return 'Variable "${}" is never used in operation "{}".'.format(variable_name, op_name)
3437
return 'Variable "${}" is never used.'.format(variable_name)

tests/core_execution/test_variables.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,15 +245,15 @@ def test_errors_on_addition_of_input_field_of_incorrect_type(self):
245245
}
246246

247247
def test_errors_on_addition_of_unknown_input_field(self):
248-
params = {'input': {'a': 'foo', 'b': 'bar', 'c': 'baz', 'f': 'dog'}}
248+
params = {'input': {'a': 'foo', 'b': 'bar', 'c': 'baz', 'extra': 'dog'}}
249249

250250
with raises(GraphQLError) as excinfo:
251251
check(self.doc, {}, params)
252252

253253
assert format_error(excinfo.value) == {
254254
'locations': [{'column': 13, 'line': 2}],
255255
'message': 'Variable "$input" got invalid value {}.\n'
256-
'In field "f": Unknown field.'.format(stringify(params['input']))
256+
'In field "extra": Unknown field.'.format(stringify(params['input']))
257257
}
258258

259259

tests/core_validation/test_fields_on_correct_type.py

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
from .utils import expect_fails_rule, expect_passes_rule
44

55

6-
def undefined_field(field, type, line, column):
6+
def undefined_field(field, type, suggestions, line, column):
77
return {
8-
'message': FieldsOnCorrectType.undefined_field_message(field, type),
8+
'message': FieldsOnCorrectType.undefined_field_message(field, type, suggestions),
99
'locations': [SourceLocation(line, column)]
1010
}
1111

@@ -71,8 +71,8 @@ def test_reports_errors_when_type_is_known_again():
7171
}
7272
},
7373
''', [
74-
undefined_field('unknown_pet_field', 'Pet', 3, 9),
75-
undefined_field('unknown_cat_field', 'Cat', 5, 13)
74+
undefined_field('unknown_pet_field', 'Pet', [], 3, 9),
75+
undefined_field('unknown_cat_field', 'Cat', [], 5, 13)
7676
])
7777

7878

@@ -82,7 +82,7 @@ def test_field_not_defined_on_fragment():
8282
meowVolume
8383
}
8484
''', [
85-
undefined_field('meowVolume', 'Dog', 3, 9)
85+
undefined_field('meowVolume', 'Dog', [], 3, 9)
8686
])
8787

8888

@@ -94,7 +94,7 @@ def test_ignores_deeply_unknown_field():
9494
}
9595
}
9696
''', [
97-
undefined_field('unknown_field', 'Dog', 3, 9)
97+
undefined_field('unknown_field', 'Dog', [], 3, 9)
9898
])
9999

100100

@@ -106,7 +106,7 @@ def test_sub_field_not_defined():
106106
}
107107
}
108108
''', [
109-
undefined_field('unknown_field', 'Pet', 4, 11)
109+
undefined_field('unknown_field', 'Pet', [], 4, 11)
110110
])
111111

112112

@@ -118,7 +118,7 @@ def test_field_not_defined_on_inline_fragment():
118118
}
119119
}
120120
''', [
121-
undefined_field('meowVolume', 'Dog', 4, 11)
121+
undefined_field('meowVolume', 'Dog', [], 4, 11)
122122
])
123123

124124

@@ -128,7 +128,7 @@ def test_aliased_field_target_not_defined():
128128
volume : mooVolume
129129
}
130130
''', [
131-
undefined_field('mooVolume', 'Dog', 3, 9)
131+
undefined_field('mooVolume', 'Dog', [], 3, 9)
132132
])
133133

134134

@@ -138,7 +138,7 @@ def test_aliased_lying_field_target_not_defined():
138138
barkVolume : kawVolume
139139
}
140140
''', [
141-
undefined_field('kawVolume', 'Dog', 3, 9)
141+
undefined_field('kawVolume', 'Dog', [], 3, 9)
142142
])
143143

144144

@@ -148,7 +148,7 @@ def test_not_defined_on_interface():
148148
tailLength
149149
}
150150
''', [
151-
undefined_field('tailLength', 'Pet', 3, 9)
151+
undefined_field('tailLength', 'Pet', [], 3, 9)
152152
])
153153

154154

@@ -158,7 +158,7 @@ def test_defined_on_implementors_but_not_on_interface():
158158
nickname
159159
}
160160
''', [
161-
undefined_field('nickname', 'Pet', 3, 9)
161+
undefined_field('nickname', 'Pet', ['Cat', 'Dog'], 3, 9)
162162
])
163163

164164

@@ -176,7 +176,7 @@ def test_direct_field_selection_on_union():
176176
directField
177177
}
178178
''', [
179-
undefined_field('directField', 'CatOrDog', 3, 9)
179+
undefined_field('directField', 'CatOrDog', [], 3, 9)
180180
])
181181

182182

@@ -186,7 +186,13 @@ def test_defined_on_implementors_queried_on_union():
186186
name
187187
}
188188
''', [
189-
undefined_field('name', 'CatOrDog', 3, 9)
189+
undefined_field(
190+
'name',
191+
'CatOrDog',
192+
['Being', 'Pet', 'Canine', 'Cat', 'Dog'],
193+
3,
194+
9
195+
)
190196
])
191197

192198

@@ -201,3 +207,27 @@ def test_valid_field_in_inline_fragment():
201207
}
202208
}
203209
''')
210+
211+
212+
def test_fields_correct_type_no_suggestion():
213+
message = FieldsOnCorrectType.undefined_field_message('T', 'f', [])
214+
assert message == 'Cannot query field "T" on type "f".'
215+
216+
217+
def test_fields_correct_type_no_small_number_suggestions():
218+
message = FieldsOnCorrectType.undefined_field_message('T', 'f', ['A', 'B'])
219+
assert message == (
220+
'Cannot query field "T" on type "f". ' +
221+
'However, this field exists on "A", "B". ' +
222+
'Perhaps you meant to use an inline fragment?'
223+
)
224+
225+
226+
def test_fields_correct_type_lot_suggestions():
227+
message = FieldsOnCorrectType.undefined_field_message('T', 'f', ['A', 'B', 'C', 'D', 'E', 'F'])
228+
assert message == (
229+
'Cannot query field "T" on type "f". ' +
230+
'However, this field exists on "A", "B", "C", "D", "E", ' +
231+
'and 1 other types. '+
232+
'Perhaps you meant to use an inline fragment?'
233+
)

tests/core_validation/test_no_unused_variables.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
from .utils import expect_fails_rule, expect_passes_rule
44

55

6-
def unused_variable(variable_name, line, column):
6+
def unused_variable(variable_name, op_name, line, column):
77
return {
8-
'message': NoUnusedVariables.unused_variable_message(variable_name),
8+
'message': NoUnusedVariables.unused_variable_message(variable_name, op_name),
99
'locations': [SourceLocation(line, column)]
1010
}
1111

1212

1313
def test_uses_all_variables():
1414
expect_passes_rule(NoUnusedVariables, '''
15-
query Foo($a: String, $b: String, $c: String) {
15+
query ($a: String, $b: String, $c: String) {
1616
field(a: $a, b: $b, c: $c)
1717
}
1818
''')
@@ -99,11 +99,11 @@ def test_variable_used_by_recursive_fragment():
9999

100100
def test_variable_not_used():
101101
expect_fails_rule(NoUnusedVariables, '''
102-
query Foo($a: String, $b: String, $c: String) {
102+
query ($a: String, $b: String, $c: String) {
103103
field(a: $a, b: $b)
104104
}
105105
''', [
106-
unused_variable('c', 2, 41)
106+
unused_variable('c', None, 2, 38)
107107
])
108108

109109

@@ -113,8 +113,8 @@ def test_multiple_variables_not_used():
113113
field(b: $b)
114114
}
115115
''', [
116-
unused_variable('a', 2, 17),
117-
unused_variable('c', 2, 41)
116+
unused_variable('a', 'Foo', 2, 17),
117+
unused_variable('c', 'Foo', 2, 41)
118118
])
119119

120120

@@ -137,7 +137,7 @@ def test_variable_not_used_in_fragments():
137137
field
138138
}
139139
''', [
140-
unused_variable('c', 2, 41)
140+
unused_variable('c', 'Foo', 2, 41)
141141
])
142142

143143

@@ -160,8 +160,8 @@ def test_multiple_variables_not_used_in_fragments():
160160
field
161161
}
162162
''', [
163-
unused_variable('a', 2, 17),
164-
unused_variable('c', 2, 41)
163+
unused_variable('a', 'Foo', 2, 17),
164+
unused_variable('c', 'Foo', 2, 41)
165165
])
166166

167167

@@ -177,7 +177,7 @@ def test_variable_not_used_by_unreferenced_fragment():
177177
field(b: $b)
178178
}
179179
''', [
180-
unused_variable('b', 2, 17),
180+
unused_variable('b', 'Foo', 2, 17),
181181
])
182182

183183

@@ -196,6 +196,6 @@ def test_variable_not_used_by_fragment_used_by_other_operation():
196196
field(b: $b)
197197
}
198198
''', [
199-
unused_variable('b', 2, 17),
200-
unused_variable('a', 5, 17),
199+
unused_variable('b', 'Foo', 2, 17),
200+
unused_variable('a', 'Bar', 5, 17),
201201
])

0 commit comments

Comments
 (0)