Skip to content

Commit 24f07c9

Browse files
jaemkcpmsmith
authored andcommitted
support null values
- addresses graphql-python#118 - initial implementation by @yen223 in PR graphql-python#119
1 parent 016d975 commit 24f07c9

32 files changed

+381
-64
lines changed

graphql/execution/executor.py

+3
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,9 @@ def resolve_field(
364364
executor = exe_context.executor
365365
result = resolve_or_error(resolve_fn_middleware, source, info, args, executor)
366366

367+
if result is Undefined:
368+
return Undefined
369+
367370
return complete_value_catching_error(
368371
exe_context, return_type, field_asts, info, field_path, result
369372
)

graphql/execution/values.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from ..utils.is_valid_value import is_valid_value
2121
from ..utils.type_from_ast import type_from_ast
2222
from ..utils.value_from_ast import value_from_ast
23+
from ..utils.undefined import Undefined
2324

2425
# Necessary for static type checking
2526
if False: # flake8: noqa
@@ -56,10 +57,11 @@ def get_variable_values(
5657
[def_ast],
5758
)
5859
elif value is None:
59-
if def_ast.default_value is not None:
60-
values[var_name] = value_from_ast(
61-
def_ast.default_value, var_type
62-
) # type: ignore
60+
if def_ast.default_value is None:
61+
values[var_name] = None
62+
elif def_ast.default_value is not Undefined:
63+
values[var_name] = value_from_ast(def_ast.default_value, var_type)
64+
6365
if isinstance(var_type, GraphQLNonNull):
6466
raise GraphQLError(
6567
'Variable "${var_name}" of required type "{var_type}" was not provided.'.format(
@@ -109,7 +111,7 @@ def get_argument_values(
109111
arg_type = arg_def.type
110112
arg_ast = arg_ast_map.get(name)
111113
if name not in arg_ast_map:
112-
if arg_def.default_value is not None:
114+
if arg_def.default_value is not Undefined:
113115
result[arg_def.out_name or name] = arg_def.default_value
114116
continue
115117
elif isinstance(arg_type, GraphQLNonNull):
@@ -123,7 +125,7 @@ def get_argument_values(
123125
variable_name = arg_ast.value.name.value # type: ignore
124126
if variables and variable_name in variables:
125127
result[arg_def.out_name or name] = variables[variable_name]
126-
elif arg_def.default_value is not None:
128+
elif arg_def.default_value is not Undefined:
127129
result[arg_def.out_name or name] = arg_def.default_value
128130
elif isinstance(arg_type, GraphQLNonNull):
129131
raise GraphQLError(
@@ -137,7 +139,7 @@ def get_argument_values(
137139
else:
138140
value = value_from_ast(arg_ast.value, arg_type, variables) # type: ignore
139141
if value is None:
140-
if arg_def.default_value is not None:
142+
if arg_def.default_value is not Undefined:
141143
value = arg_def.default_value
142144
result[arg_def.out_name or name] = value
143145
else:
@@ -172,7 +174,7 @@ def coerce_value(type, value):
172174
obj = {}
173175
for field_name, field in fields.items():
174176
if field_name not in value:
175-
if field.default_value is not None:
177+
if field.default_value is not Undefined:
176178
field_value = field.default_value
177179
obj[field.out_name or field_name] = field_value
178180
else:

graphql/language/ast.py

+42
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,48 @@ def __hash__(self):
604604
return id(self)
605605

606606

607+
class NullValue(Value):
608+
__slots__ = ("loc", "value")
609+
_fields = ("value",)
610+
611+
def __init__(self, value=None, loc=None):
612+
self.value = None
613+
self.loc = loc
614+
615+
def __eq__(self, other):
616+
return isinstance(other, NullValue)
617+
618+
def __repr__(self):
619+
return "NullValue"
620+
621+
def __copy__(self):
622+
return type(self)(self.value, self.loc)
623+
624+
def __hash__(self):
625+
return id(self)
626+
627+
628+
class UndefinedValue(Value):
629+
__slots__ = ("loc", "value")
630+
_fields = ("value",)
631+
632+
def __init__(self, value=None, loc=None):
633+
self.value = None
634+
self.loc = loc
635+
636+
def __eq__(self, other):
637+
return isinstance(other, UndefinedValue)
638+
639+
def __repr__(self):
640+
return "UndefinedValue"
641+
642+
def __copy__(self):
643+
return type(self)(self.value, self.loc)
644+
645+
def __hash__(self):
646+
return id(self)
647+
648+
607649
class EnumValue(Value):
608650
__slots__ = ("loc", "value")
609651
_fields = ("value",)

graphql/language/parser.py

+17-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from ..error import GraphQLSyntaxError
55
from .lexer import Lexer, TokenKind, get_token_desc, get_token_kind_desc
66
from .source import Source
7+
from ..utils.undefined import Undefined
78

89
# Necessary for static type checking
910
if False: # flake8: noqa
@@ -65,6 +66,12 @@ def parse(source, **kwargs):
6566

6667

6768
def parse_value(source, **kwargs):
69+
if source is None:
70+
return ast.NullValue()
71+
72+
if source is Undefined:
73+
return ast.UndefinedValue()
74+
6875
options = {"no_location": False, "no_source": False}
6976
options.update(kwargs)
7077
source_obj = source
@@ -338,7 +345,7 @@ def parse_variable_definition(parser):
338345
type=expect(parser, TokenKind.COLON) and parse_type(parser),
339346
default_value=parse_value_literal(parser, True)
340347
if skip(parser, TokenKind.EQUALS)
341-
else None,
348+
else Undefined,
342349
loc=loc(parser, start),
343350
)
344351

@@ -493,18 +500,21 @@ def parse_value_literal(parser, is_const):
493500
)
494501

495502
elif token.kind == TokenKind.NAME:
503+
advance(parser)
496504
if token.value in ("true", "false"):
497-
advance(parser)
498505
return ast.BooleanValue(
499506
value=token.value == "true", loc=loc(parser, token.start)
500507
)
501508

502-
if token.value != "null":
503-
advance(parser)
504-
return ast.EnumValue(
505-
value=token.value, loc=loc(parser, token.start) # type: ignore
509+
if token.value == "null":
510+
return ast.NullValue(
511+
loc=loc(parser, token.start) # type: ignore
506512
)
507513

514+
return ast.EnumValue( # type: ignore
515+
value=token.value, loc=loc(parser, token.start)
516+
)
517+
508518
elif token.kind == TokenKind.DOLLAR:
509519
if not is_const:
510520
return parse_variable(parser)
@@ -754,7 +764,7 @@ def parse_input_value_def(parser):
754764
type=expect(parser, TokenKind.COLON) and parse_type(parser), # type: ignore
755765
default_value=parse_const_value(parser)
756766
if skip(parser, TokenKind.EQUALS)
757-
else None,
767+
else Undefined,
758768
directives=parse_directives(parser),
759769
loc=loc(parser, start),
760770
)

graphql/language/printer.py

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22

33
from .visitor import Visitor, visit
4+
from ..utils.undefined import Undefined
45

56
# Necessary for static type checking
67
if False: # flake8: noqa
@@ -45,7 +46,7 @@ def leave_OperationDefinition(self, node, *args):
4546

4647
def leave_VariableDefinition(self, node, *args):
4748
# type: (Any, *Any) -> str
48-
return node.variable + ": " + node.type + wrap(" = ", node.default_value)
49+
return node.variable + ": " + node.type + wrap(" = ", node.default_value, is_default_value=True)
4950

5051
def leave_SelectionSet(self, node, *args):
5152
# type: (Any, *Any) -> str
@@ -111,6 +112,12 @@ def leave_BooleanValue(self, node, *args):
111112
# type: (Any, *Any) -> str
112113
return json.dumps(node.value)
113114

115+
def leave_NullValue(self, node, *args):
116+
return "null"
117+
118+
def leave_UndefinedValue(self, node, *args):
119+
return Undefined
120+
114121
def leave_EnumValue(self, node, *args):
115122
# type: (Any, *Any) -> str
116123
return node.value
@@ -192,7 +199,7 @@ def leave_InputValueDefinition(self, node, *args):
192199
node.name
193200
+ ": "
194201
+ node.type
195-
+ wrap(" = ", node.default_value)
202+
+ wrap(" = ", node.default_value, is_default_value=True)
196203
+ wrap(" ", join(node.directives, " "))
197204
)
198205

@@ -232,13 +239,14 @@ def leave_EnumValueDefinition(self, node, *args):
232239

233240
def leave_InputObjectTypeDefinition(self, node, *args):
234241
# type: (Any, *Any) -> str
235-
return (
242+
s = (
236243
"input "
237244
+ node.name
238245
+ wrap(" ", join(node.directives, " "))
239246
+ " "
240247
+ block(node.fields)
241248
)
249+
return s
242250

243251
def leave_TypeExtensionDefinition(self, node, *args):
244252
# type: (Any, *Any) -> str
@@ -268,8 +276,14 @@ def block(_list):
268276
return "{}"
269277

270278

271-
def wrap(start, maybe_str, end=""):
272-
# type: (str, Optional[str], str) -> str
279+
def wrap(start, maybe_str, end="", is_default_value=False):
280+
# type: (str, Optional[str], str, bool) -> str
281+
if is_default_value:
282+
if maybe_str is Undefined:
283+
return ""
284+
s = "null" if maybe_str is None else maybe_str
285+
return start + s + end
286+
273287
if maybe_str:
274288
return start + maybe_str + end
275289
return ""

graphql/language/tests/fixtures.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
}
5454
5555
{
56-
unnamed(truthy: true, falsey: false),
56+
unnamed(truthy: true, falsey: false, nullish: null),
5757
query
5858
}
5959
"""

graphql/language/tests/test_parser.py

+65-4
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,72 @@ def test_does_not_accept_fragments_spread_of_on():
101101
assert "Syntax Error GraphQL (1:9) Expected Name, found }" in excinfo.value.message
102102

103103

104-
def test_does_not_allow_null_value():
104+
def test_allows_null_value():
105105
# type: () -> None
106-
with raises(GraphQLSyntaxError) as excinfo:
107-
parse("{ fieldWithNullableStringInput(input: null) }")
106+
parse("{ fieldWithNullableStringInput(input: null) }")
107+
108+
109+
def test_parses_null_value_to_null():
110+
result = parse('{ fieldWithObjectInput(input: {a: null, b: null, c: "C", d: null}) }')
111+
values = result.definitions[0].selection_set.selections[0].arguments[0].value.fields
112+
expected = (
113+
(u"a", ast.NullValue()),
114+
(u"b", ast.NullValue()),
115+
(u"c", ast.StringValue(value=u"C")),
116+
(u"d", ast.NullValue()),
117+
)
118+
for name_value, actual in zip(expected, values):
119+
assert name_value == (actual.name.value, actual.value)
120+
121+
122+
def test_parses_null_value_in_list():
123+
result = parse('{ fieldWithObjectInput(input: {b: ["A", null, "C"], c: "C"}) }')
124+
assert result == ast.Document(
125+
definitions=[
126+
ast.OperationDefinition(
127+
operation="query", name=None, variable_definitions=None, directives=[],
128+
selection_set=ast.SelectionSet(
129+
selections=[
130+
ast.Field(
131+
alias=None,
132+
name=ast.Name(value=u"fieldWithObjectInput"),
133+
directives=[],
134+
selection_set=None,
135+
arguments=[
136+
ast.Argument(
137+
name=ast.Name(value=u"input"),
138+
value=ast.ObjectValue(
139+
fields=[
140+
ast.ObjectField(
141+
name=ast.Name(value=u"b"),
142+
value=ast.ListValue(
143+
values=[
144+
ast.StringValue(value=u"A"),
145+
ast.NullValue(),
146+
ast.StringValue(value=u"C"),
147+
],
148+
),
149+
),
150+
ast.ObjectField(
151+
name=ast.Name(value=u"c"),
152+
value=ast.StringValue(value=u"C"),
153+
),
154+
]
155+
),
156+
),
157+
],
158+
),
159+
],
160+
),
161+
),
162+
],
163+
)
164+
108165

109-
assert 'Syntax Error GraphQL (1:39) Unexpected Name "null"' in excinfo.value.message
166+
def test_null_as_name():
167+
result = parse('{ thingy(null: "stringcheese") }')
168+
assert result.definitions[0].selection_set.selections[0].name.value == "thingy"
169+
assert result.definitions[0].selection_set.selections[0].arguments[0].name.value == "null"
110170

111171

112172
def test_parses_multi_byte_characters():
@@ -158,6 +218,7 @@ def tesst_allows_non_keywords_anywhere_a_name_is_allowed():
158218
"subscription",
159219
"true",
160220
"false",
221+
"null",
161222
]
162223

163224
query_template = """

graphql/language/tests/test_printer.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ def test_correctly_prints_mutation_with_artifacts():
8585
)
8686

8787

88+
def test_correctly_prints_null():
89+
query_ast_shorthanded = parse('{ thingy(null: "wow", name: null) }')
90+
assert print_ast(query_ast_shorthanded) == """{
91+
thingy(null: "wow", name: null)
92+
}
93+
"""
94+
95+
8896
def test_prints_kitchen_sink():
8997
# type: () -> None
9098
ast = parse(KITCHEN_SINK)
@@ -138,7 +146,7 @@ def test_prints_kitchen_sink():
138146
}
139147
140148
{
141-
unnamed(truthy: true, falsey: false)
149+
unnamed(truthy: true, falsey: false, nullish: null)
142150
query
143151
}
144152
"""

0 commit comments

Comments
 (0)