Skip to content

Commit fdc03a3

Browse files
authored
Merge pull request #132 from graphql-python/features/undefined-arguments
Better variable value coercion and input object containers support
2 parents 67f18aa + 427305c commit fdc03a3

File tree

13 files changed

+168
-106
lines changed

13 files changed

+168
-106
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ language: python
22
sudo: false
33
python:
44
- 2.7
5-
- 3.3
65
- 3.4
76
- 3.5
7+
- 3.6
88
- pypy
99
before_install:
1010
- |

graphql/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@
202202

203203
# Asserts a string is a valid GraphQL name.
204204
assert_valid_name,
205+
206+
# Undefined const
207+
Undefined,
205208
)
206209

207210
__all__ = (
@@ -284,4 +287,5 @@
284287
'type_from_ast',
285288
'value_from_ast',
286289
'get_version',
290+
'Undefined',
287291
)

graphql/execution/base.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from ..error import GraphQLError
55
from ..language import ast
66
from ..pyutils.default_ordered_dict import DefaultOrderedDict
7-
from ..type.definition import Undefined, GraphQLInterfaceType, GraphQLUnionType
7+
from ..type.definition import GraphQLInterfaceType, GraphQLUnionType
88
from ..type.directives import GraphQLIncludeDirective, GraphQLSkipDirective
99
from ..type.introspection import (SchemaMetaFieldDef, TypeMetaFieldDef,
1010
TypeNameMetaFieldDef)
@@ -75,7 +75,6 @@ def get_field_resolver(self, field_resolver):
7575
def get_argument_values(self, field_def, field_ast):
7676
k = field_def, field_ast
7777
result = self.argument_values_cache.get(k)
78-
7978
if not result:
8079
result = self.argument_values_cache[k] = get_argument_values(field_def.args, field_ast.arguments,
8180
self.variable_values)

graphql/execution/executor.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
from ..error import GraphQLError, GraphQLLocatedError
1010
from ..pyutils.default_ordered_dict import DefaultOrderedDict
1111
from ..pyutils.ordereddict import OrderedDict
12+
from ..utils.undefined import Undefined
1213
from ..type import (GraphQLEnumType, GraphQLInterfaceType, GraphQLList,
1314
GraphQLNonNull, GraphQLObjectType, GraphQLScalarType,
1415
GraphQLSchema, GraphQLUnionType)
15-
from .base import (ExecutionContext, ExecutionResult, ResolveInfo, Undefined,
16+
from .base import (ExecutionContext, ExecutionResult, ResolveInfo,
1617
collect_fields, default_resolve_fn, get_field_def,
1718
get_operation_root_type)
1819
from .executors.sync import SyncExecutor

graphql/execution/experimental/fragment.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
from ...pyutils.cached_property import cached_property
66
from ...pyutils.default_ordered_dict import DefaultOrderedDict
7+
from ...utils.undefined import Undefined
78
from ...type import (GraphQLInterfaceType, GraphQLList, GraphQLNonNull,
89
GraphQLObjectType, GraphQLUnionType)
9-
from ..base import ResolveInfo, Undefined, collect_fields, get_field_def
10+
from ..base import ResolveInfo, collect_fields, get_field_def
1011
from ..values import get_argument_values
1112
from ...error import GraphQLError
1213
try:

graphql/execution/experimental/tests/test_variables.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -413,9 +413,10 @@ def test_passes_along_null_for_non_nullable_inputs_if_explcitly_set_in_the_query
413413
'''
414414

415415
check(doc, {
416-
'data': {
417-
'fieldWithNonNullableStringInput': None
418-
}
416+
'errors': [{
417+
'message': 'Argument "input" of required type String!" was not provided.'
418+
}],
419+
'data': None
419420
})
420421

421422

graphql/execution/tests/test_variables.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from graphql.error import GraphQLError, format_error
77
from graphql.execution import execute
88
from graphql.language.parser import parse
9-
from graphql.type import (GraphQLArgument, GraphQLField,
9+
from graphql.type import (GraphQLArgument, GraphQLField, GraphQLBoolean,
1010
GraphQLInputObjectField, GraphQLInputObjectType,
1111
GraphQLList, GraphQLNonNull, GraphQLObjectType,
1212
GraphQLScalarType, GraphQLSchema, GraphQLString)
@@ -18,13 +18,23 @@
1818
parse_literal=lambda v: 'DeserializedValue' if v.value == 'SerializedValue' else None
1919
)
2020

21+
22+
class my_special_dict(dict):
23+
pass
24+
25+
2126
TestInputObject = GraphQLInputObjectType('TestInputObject', OrderedDict([
2227
('a', GraphQLInputObjectField(GraphQLString)),
2328
('b', GraphQLInputObjectField(GraphQLList(GraphQLString))),
2429
('c', GraphQLInputObjectField(GraphQLNonNull(GraphQLString))),
2530
('d', GraphQLInputObjectField(TestComplexScalar))
2631
]))
2732

33+
34+
TestCustomInputObject = GraphQLInputObjectType('TestCustomInputObject', OrderedDict([
35+
('a', GraphQLInputObjectField(GraphQLString)),
36+
]), container_type=my_special_dict)
37+
2838
stringify = lambda obj: json.dumps(obj, sort_keys=True)
2939

3040

@@ -47,6 +57,10 @@ def input_to_json(obj, args, context, info):
4757
GraphQLString,
4858
args={'input': GraphQLArgument(TestInputObject)},
4959
resolver=input_to_json),
60+
'fieldWithCustomObjectInput': GraphQLField(
61+
GraphQLBoolean,
62+
args={'input': GraphQLArgument(TestCustomInputObject)},
63+
resolver=lambda root, args, context, info: isinstance(args.get('input'), my_special_dict)),
5064
'fieldWithNullableStringInput': GraphQLField(
5165
GraphQLString,
5266
args={'input': GraphQLArgument(GraphQLString)},
@@ -412,9 +426,24 @@ def test_passes_along_null_for_non_nullable_inputs_if_explcitly_set_in_the_query
412426
}
413427
'''
414428

429+
check(doc, {
430+
'errors': [{
431+
'message': 'Argument "input" of required type String!" was not provided.'
432+
}],
433+
'data': None
434+
})
435+
436+
437+
def test_uses_objectinput_container():
438+
doc = '''
439+
{
440+
fieldWithCustomObjectInput(input: {a: "b"})
441+
}
442+
'''
443+
415444
check(doc, {
416445
'data': {
417-
'fieldWithNonNullableStringInput': None
446+
'fieldWithCustomObjectInput': True
418447
}
419448
})
420449

graphql/execution/values.py

Lines changed: 84 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from six import string_types
55

66
from ..error import GraphQLError
7+
from ..language import ast
78
from ..language.printer import print_ast
89
from ..type import (GraphQLEnumType, GraphQLInputObjectType, GraphQLList,
910
GraphQLNonNull, GraphQLScalarType, is_input_type)
@@ -23,8 +24,43 @@ def get_variable_values(schema, definition_asts, inputs):
2324
values = {}
2425
for def_ast in definition_asts:
2526
var_name = def_ast.variable.name.value
26-
value = get_variable_value(schema, def_ast, inputs.get(var_name))
27-
values[var_name] = value
27+
var_type = type_from_ast(schema, def_ast.type)
28+
value = inputs.get(var_name)
29+
30+
if not is_input_type(var_type):
31+
raise GraphQLError(
32+
'Variable "${var_name}" expected value of type "{var_type}" which cannot be used as an input type.'.format(
33+
var_name=var_name,
34+
var_type=print_ast(def_ast.type),
35+
),
36+
[def_ast]
37+
)
38+
elif value is None:
39+
if def_ast.default_value is not None:
40+
values[var_name] = value_from_ast(def_ast.default_value, var_type)
41+
if isinstance(var_type, GraphQLNonNull):
42+
raise GraphQLError(
43+
'Variable "${var_name}" of required type "{var_type}" was not provided.'.format(
44+
var_name=var_name, var_type=var_type
45+
), [def_ast]
46+
)
47+
else:
48+
errors = is_valid_value(value, var_type)
49+
if errors:
50+
message = u'\n' + u'\n'.join(errors)
51+
raise GraphQLError(
52+
'Variable "${}" got invalid value {}.{}'.format(
53+
var_name,
54+
json.dumps(value, sort_keys=True),
55+
message
56+
),
57+
[def_ast]
58+
)
59+
coerced_value = coerce_value(var_type, value)
60+
if coerced_value is None:
61+
raise Exception('Should have reported error.')
62+
63+
values[var_name] = coerced_value
2864

2965
return values
3066

@@ -42,72 +78,52 @@ def get_argument_values(arg_defs, arg_asts, variables=None):
4278

4379
result = {}
4480
for name, arg_def in arg_defs.items():
81+
arg_type = arg_def.type
4582
value_ast = arg_ast_map.get(name)
46-
if value_ast:
47-
value_ast = value_ast.value
48-
49-
value = value_from_ast(
50-
value_ast,
51-
arg_def.type,
52-
variables
53-
)
83+
if name not in arg_ast_map:
84+
if arg_def.default_value is not None:
85+
result[arg_def.out_name or name] = arg_def.default_value
86+
continue
87+
elif isinstance(arg_type, GraphQLNonNull):
88+
raise GraphQLError('Argument "{name}" of required type {arg_type}" was not provided.'.format(
89+
name=name,
90+
arg_type=arg_type
91+
), arg_asts)
92+
elif isinstance(value_ast.value, ast.Variable):
93+
variable_name = value_ast.value.name.value
94+
variable_value = variables.get(variable_name)
95+
if variables and variable_name in variables:
96+
result[arg_def.out_name or name] = variable_value
97+
elif arg_def.default_value is not None:
98+
result[arg_def.out_name or name] = arg_def.default_value
99+
elif isinstance(arg_type, GraphQLNonNull):
100+
raise GraphQLError('Argument "{name}" of required type {arg_type}" provided the variable "${variable_name}" which was not provided'.format(
101+
name=name,
102+
arg_type=arg_type,
103+
variable_name=variable_name
104+
), arg_asts)
105+
continue
54106

55-
if value is None:
56-
value = arg_def.default_value
107+
else:
108+
value_ast = value_ast.value
57109

58-
if value is not None:
59-
# We use out_name as the output name for the
60-
# dict if exists
61-
result[arg_def.out_name or name] = value
110+
value = value_from_ast(
111+
value_ast,
112+
arg_type,
113+
variables
114+
)
115+
if value is None:
116+
if arg_def.default_value is not None:
117+
value = arg_def.default_value
118+
result[arg_def.out_name or name] = value
119+
else:
120+
# We use out_name as the output name for the
121+
# dict if exists
122+
result[arg_def.out_name or name] = value
62123

63124
return result
64125

65126

66-
def get_variable_value(schema, definition_ast, input):
67-
"""Given a variable definition, and any value of input, return a value which adheres to the variable definition,
68-
or throw an error."""
69-
type = type_from_ast(schema, definition_ast.type)
70-
variable = definition_ast.variable
71-
72-
if not type or not is_input_type(type):
73-
raise GraphQLError(
74-
'Variable "${}" expected value of type "{}" which cannot be used as an input type.'.format(
75-
variable.name.value,
76-
print_ast(definition_ast.type),
77-
),
78-
[definition_ast]
79-
)
80-
81-
input_type = type
82-
errors = is_valid_value(input, input_type)
83-
if not errors:
84-
if input is None:
85-
default_value = definition_ast.default_value
86-
if default_value:
87-
return value_from_ast(default_value, input_type)
88-
89-
return coerce_value(input_type, input)
90-
91-
if input is None:
92-
raise GraphQLError(
93-
'Variable "${}" of required type "{}" was not provided.'.format(
94-
variable.name.value,
95-
print_ast(definition_ast.type)
96-
),
97-
[definition_ast]
98-
)
99-
100-
message = (u'\n' + u'\n'.join(errors)) if errors else u''
101-
raise GraphQLError(
102-
'Variable "${}" got invalid value {}.{}'.format(
103-
variable.name.value,
104-
json.dumps(input, sort_keys=True),
105-
message
106-
),
107-
[definition_ast]
108-
)
109-
110-
111127
def coerce_value(type, value):
112128
"""Given a type and any value, return a runtime value coerced to match the type."""
113129
if isinstance(type, GraphQLNonNull):
@@ -130,16 +146,15 @@ def coerce_value(type, value):
130146
fields = type.fields
131147
obj = {}
132148
for field_name, field in fields.items():
133-
field_value = coerce_value(field.type, value.get(field_name))
134-
if field_value is None:
135-
field_value = field.default_value
136-
137-
if field_value is not None:
138-
# We use out_name as the output name for the
139-
# dict if exists
149+
if field_name not in value:
150+
if field.default_value is not None:
151+
field_value = field.default_value
152+
obj[field.out_name or field_name] = field_value
153+
else:
154+
field_value = coerce_value(field.type, value.get(field_name))
140155
obj[field.out_name or field_name] = field_value
141156

142-
return obj
157+
return type.create_container(obj)
143158

144159
assert isinstance(type, (GraphQLScalarType, GraphQLEnumType)), \
145160
'Must be input type'

graphql/type/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
is_leaf_type,
2020
is_type,
2121
get_nullable_type,
22-
is_output_type,
23-
Undefined
22+
is_output_type
2423
)
2524
from .directives import (
2625
# "Enum" of Directive locations

graphql/type/definition.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,6 @@
77
from ..utils.assert_valid_name import assert_valid_name
88

99

10-
class _Undefined(object):
11-
def __bool__(self):
12-
return False
13-
14-
__nonzero__ = __bool__
15-
16-
17-
Undefined = _Undefined()
18-
19-
2010
def is_type(type):
2111
return isinstance(type, (
2212
GraphQLScalarType,
@@ -516,13 +506,19 @@ class GeoPoint(GraphQLInputObjectType):
516506
default_value=0)
517507
}
518508
"""
519-
def __init__(self, name, fields, description=None):
509+
def __init__(self, name, fields, description=None, container_type=None):
520510
assert name, 'Type must be named.'
521511
self.name = name
522512
self.description = description
523-
513+
if container_type is None:
514+
container_type = dict
515+
assert callable(container_type), "container_type must be callable"
516+
self.container_type = container_type
524517
self._fields = fields
525518

519+
def create_container(self, data):
520+
return self.container_type(data)
521+
526522
@cached_property
527523
def fields(self):
528524
return self._define_field_map()

0 commit comments

Comments
 (0)