Skip to content

Commit 8b84773

Browse files
committed
support null values
- addresses graphql-python#118 - initial implementation by @yen223 in PR graphql-python#119
1 parent 9202021 commit 8b84773

32 files changed

+381
-64
lines changed

graphql/execution/executor.py

+3
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,9 @@ def resolve_field(
359359
executor = exe_context.executor
360360
result = resolve_or_error(resolve_fn_middleware, source, info, args, executor)
361361

362+
if result is Undefined:
363+
return Undefined
364+
362365
return complete_value_catching_error(
363366
exe_context, return_type, field_asts, info, field_path, result
364367
)

graphql/execution/values.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ..utils.is_valid_value import is_valid_value
1818
from ..utils.type_from_ast import type_from_ast
1919
from ..utils.value_from_ast import value_from_ast
20+
from ..utils.undefined import Undefined
2021

2122
# Necessary for static type checking
2223
if False: # flake8: noqa
@@ -53,10 +54,11 @@ def get_variable_values(
5354
[def_ast],
5455
)
5556
elif value is None:
56-
if def_ast.default_value is not None:
57-
values[var_name] = value_from_ast(
58-
def_ast.default_value, var_type
59-
) # type: ignore
57+
if def_ast.default_value is None:
58+
values[var_name] = None
59+
elif def_ast.default_value is not Undefined:
60+
values[var_name] = value_from_ast(def_ast.default_value, var_type)
61+
6062
if isinstance(var_type, GraphQLNonNull):
6163
raise GraphQLError(
6264
'Variable "${var_name}" of required type "{var_type}" was not provided.'.format(
@@ -106,7 +108,7 @@ def get_argument_values(
106108
arg_type = arg_def.type
107109
arg_ast = arg_ast_map.get(name)
108110
if name not in arg_ast_map:
109-
if arg_def.default_value is not None:
111+
if arg_def.default_value is not Undefined:
110112
result[arg_def.out_name or name] = arg_def.default_value
111113
continue
112114
elif isinstance(arg_type, GraphQLNonNull):
@@ -120,7 +122,7 @@ def get_argument_values(
120122
variable_name = arg_ast.value.name.value # type: ignore
121123
if variables and variable_name in variables:
122124
result[arg_def.out_name or name] = variables[variable_name]
123-
elif arg_def.default_value is not None:
125+
elif arg_def.default_value is not Undefined:
124126
result[arg_def.out_name or name] = arg_def.default_value
125127
elif isinstance(arg_type, GraphQLNonNull):
126128
raise GraphQLError(
@@ -134,7 +136,7 @@ def get_argument_values(
134136
else:
135137
value = value_from_ast(arg_ast.value, arg_type, variables) # type: ignore
136138
if value is None:
137-
if arg_def.default_value is not None:
139+
if arg_def.default_value is not Undefined:
138140
value = arg_def.default_value
139141
result[arg_def.out_name or name] = value
140142
else:
@@ -171,7 +173,7 @@ def coerce_value(type, value):
171173
obj = {}
172174
for field_name, field in fields.items():
173175
if field_name not in value:
174-
if field.default_value is not None:
176+
if field.default_value is not Undefined:
175177
field_value = field.default_value
176178
obj[field.out_name or field_name] = field_value
177179
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
@@ -67,6 +68,12 @@ def parse(source, **kwargs):
6768

6869

6970
def parse_value(source, **kwargs):
71+
if source is None:
72+
return ast.NullValue()
73+
74+
if source is Undefined:
75+
return ast.UndefinedValue()
76+
7077
options = {"no_location": False, "no_source": False}
7178
options.update(kwargs)
7279
source_obj = source
@@ -340,7 +347,7 @@ def parse_variable_definition(parser):
340347
type=expect(parser, TokenKind.COLON) and parse_type(parser),
341348
default_value=parse_value_literal(parser, True)
342349
if skip(parser, TokenKind.EQUALS)
343-
else None,
350+
else Undefined,
344351
loc=loc(parser, start),
345352
)
346353

@@ -495,18 +502,21 @@ def parse_value_literal(parser, is_const):
495502
)
496503

497504
elif token.kind == TokenKind.NAME:
505+
advance(parser)
498506
if token.value in ("true", "false"):
499-
advance(parser)
500507
return ast.BooleanValue( # type: ignore
501508
value=token.value == "true", loc=loc(parser, token.start)
502509
)
503510

504-
if token.value != "null":
505-
advance(parser)
506-
return ast.EnumValue( # type: ignore
507-
value=token.value, loc=loc(parser, token.start)
511+
if token.value == "null":
512+
return ast.NullValue( # type: ignore
513+
loc=loc(parser, token.start)
508514
)
509515

516+
return ast.EnumValue( # type: ignore
517+
value=token.value, loc=loc(parser, token.start)
518+
)
519+
510520
elif token.kind == TokenKind.DOLLAR:
511521
if not is_const:
512522
return parse_variable(parser)
@@ -756,7 +766,7 @@ def parse_input_value_def(parser):
756766
type=expect(parser, TokenKind.COLON) and parse_type(parser),
757767
default_value=parse_const_value(parser)
758768
if skip(parser, TokenKind.EQUALS)
759-
else None,
769+
else Undefined,
760770
directives=parse_directives(parser),
761771
loc=loc(parser, start),
762772
)

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
@@ -82,7 +83,7 @@ def leave_OperationDefinition(self, node, *args):
8283

8384
def leave_VariableDefinition(self, node, *args):
8485
# type: (Any, *Any) -> str
85-
return node.variable + ": " + node.type + wrap(" = ", node.default_value)
86+
return node.variable + ": " + node.type + wrap(" = ", node.default_value, is_default_value=True)
8687

8788
def leave_SelectionSet(self, node, *args):
8889
# type: (Any, *Any) -> str
@@ -148,6 +149,12 @@ def leave_BooleanValue(self, node, *args):
148149
# type: (Any, *Any) -> str
149150
return json.dumps(node.value)
150151

152+
def leave_NullValue(self, node, *args):
153+
return "null"
154+
155+
def leave_UndefinedValue(self, node, *args):
156+
return Undefined
157+
151158
def leave_EnumValue(self, node, *args):
152159
# type: (Any, *Any) -> str
153160
return node.value
@@ -229,7 +236,7 @@ def leave_InputValueDefinition(self, node, *args):
229236
node.name
230237
+ ": "
231238
+ node.type
232-
+ wrap(" = ", node.default_value)
239+
+ wrap(" = ", node.default_value, is_default_value=True)
233240
+ wrap(" ", join(node.directives, " "))
234241
)
235242

@@ -269,13 +276,14 @@ def leave_EnumValueDefinition(self, node, *args):
269276

270277
def leave_InputObjectTypeDefinition(self, node, *args):
271278
# type: (Any, *Any) -> str
272-
return (
279+
s = (
273280
"input "
274281
+ node.name
275282
+ wrap(" ", join(node.directives, " "))
276283
+ " "
277284
+ block(node.fields)
278285
)
286+
return s
279287

280288
def leave_TypeExtensionDefinition(self, node, *args):
281289
# type: (Any, *Any) -> str
@@ -305,8 +313,14 @@ def block(_list):
305313
return "{}"
306314

307315

308-
def wrap(start, maybe_str, end=""):
309-
# type: (str, Optional[str], str) -> str
316+
def wrap(start, maybe_str, end="", is_default_value=False):
317+
# type: (str, Optional[str], str, bool) -> str
318+
if is_default_value:
319+
if maybe_str is Undefined:
320+
return ""
321+
s = "null" if maybe_str is None else maybe_str
322+
return start + s + end
323+
310324
if maybe_str:
311325
return start + maybe_str + end
312326
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)