8000 Merge remote-tracking branch 'jaemk/add_nullvalue' into fellow-backports · fellowapp/graphql-core-legacy@5cff60e · GitHub
[go: up one dir, main page]

Skip to content

Commit 5cff60e

Browse files
committed
Merge remote-tracking branch 'jaemk/add_nullvalue' into fellow-backports
Adds support for null literals Open PR here: graphql-python#172
2 parents 016d975 + 6329736 commit 5cff60e

35 files changed

+485
-89
lines changed

graphql/execution/executor.py

Lines changed: 3 additions & 0 deletions
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/tests/test_execute_schema.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# type: ignore
22

3+
from itertools import starmap, repeat
4+
from typing import Union
35
from graphql.execution import execute
46
from graphql.language.parser import parse
57
from graphql.type import (
@@ -51,36 +53,44 @@ def test_executes_using_a_schema():
5153
{
5254
"id": GraphQLField(GraphQLNonNull(GraphQLString)),
5355
"isPublished": GraphQLField(GraphQLBoolean),
56+
"topic": GraphQLField(GraphQLString),
5457
"author": GraphQLField(BlogAuthor),
5558
"title": GraphQLField(GraphQLString),
5659
"body": GraphQLField(GraphQLString),
5760
"keywords": GraphQLField(GraphQLList(GraphQLString)),
5861
},
5962
)
6063

64+
def _resolve_article(obj, info, id, topic):
65+
return Article(id, topic)
66+
67+
def _resolve_feed(*_):
68+
return list(starmap(Article, zip(range(1, 10 + 1), repeat("food"))))
69+
6170
BlogQuery = GraphQLObjectType(
6271
"Query",
6372
{
6473
"article": GraphQLField(
6574
BlogArticle,
66-
args={"id": GraphQLArgument(GraphQLID)},
67-
resolver=lambda obj, info, **args: Article(args["id"]),
68-
),
69-
"feed": GraphQLField(
70-
GraphQLList(BlogArticle),
71-
resolver=lambda *_: map(Article, range(1, 10 + 1)),
75+
args={
76+
"id": GraphQLArgument(GraphQLID),
77+
"topic": GraphQLArgument(GraphQLNonNull(GraphQLString)),
78+
},
79+
resolver=_resolve_article,
7280
),
81+
"feed": GraphQLField(GraphQLList(BlogArticle), resolver=_resolve_feed),
7382
},
7483
)
7584

7685
BlogSchema = GraphQLSchema(BlogQuery)
7786

7887
class Article(object):
79-
def __init__(self, id):
80-
# type: (int) -> None
88+
def __init__(self, id, topic):
89+
# type: (int, Union[str, None]) -> None
8190
self.id = id
8291
self.isPublished = True
8392
self.author = Author()
93+
self.topic = "My topic is {}".format(topic or "null")
8494
self.title = "My Article {}".format(id)
8595
self.body = "This is a post"
8696
self.hidden = "This data is not exposed in the schema"
@@ -97,7 +107,7 @@ def pic(self, width, height):
97107
@property
98108
def recentArticle(self):
99109
# type: () -> Article
100-
return Article(1)
110+
return Article(1, "food")
101111

102112
class Pic(object):
103113
def __init__(self, uid, width, height):
@@ -112,7 +122,7 @@ def __init__(self, uid, width, height):
112122
id,
113123
title
114124
},
115-
article(id: "1") {
125+
article(id: "1", topic: null) {
116126
...articleFields,
117127
author {
118128
id,
@@ -132,6 +142,7 @@ def __init__(self, uid, width, height):
132142
fragment articleFields on Article {
133143
id,
134144
isPublished,
145+
topic,
135146
title,
136147
body,
137148
hidden,
@@ -159,6 +170,7 @@ def __init__(self, uid, width, height):
159170
"article": {
160171
"id": "1",
161172
"isPublished": True,
173+
"topic": "My topic is null",
162174
"title": "My Article 1",
163175
"body": "This is a post",
164176
"author": {
@@ -168,6 +180,7 @@ def __init__(self, uid, width, height):
168180
"recentArticle": {
169181
"id": "1",
170182
"isPublished": True,
183+
"topic": "My topic is food",
171184
"title": "My Article 1",
172185
"body": "This is a post",
173186
"keywords": ["foo", "bar", "1", "true", None],

graphql/execution/tests/test_mutations.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,16 @@ def assert_evaluate_mutations_serially(executor=None):
107107
},
108108
fifth: immediatelyChangeTheNumber(newNumber: 5) {
109109
theNumber
110-
}
110+
},
111+
sixth: immediatelyChangeTheNumber(newNumber: null) {
112+
theNumber
113+
},
114+
seventh: immediatelyChangeTheNumber(newNumber: 100) {
115+
theNumber
116+
},
117+
eighth: immediatelyChangeTheNumber(newNumber: null) {
118+
theNumber
119+
},
111120
}"""
112121
ast = parse(doc)
113122
result = execute(schema, ast, Root(6), operation_name="M", executor=executor)
@@ -118,6 +127,9 @@ def assert_evaluate_mutations_serially(executor=None):
118127
"third": {"theNumber": 3},
119128
"fourth": {"theNumber": 4},
120129
"fifth": {"theNumber": 5},
130+
"sixth": {"theNumber": None},
131+
"seventh": {"theNumber": 100},
132+
"eighth": {"theNumber": None},
121133
}
122134

123135

graphql/execution/tests/test_resolve.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,27 @@ def _test_schema(test_field):
4949
)
5050

5151

52+
def test_explicit_null_is_passed_to_resolver():
53+
def resolver(_, __, maybe_string):
54+
return 'maybe_string is "{}"'.format(maybe_string or "null")
55+
56+
schema = _test_schema(
57+
GraphQLField(
58+
GraphQLString,
59+
args=OrderedDict([("maybe_string", GraphQLArgument(GraphQLString))]),
60+
resolver=resolver,
61+
)
62+
)
63+
64+
result = graphql(schema, '{ test(maybe_string: "Cool") }')
65+
assert not result.errors
66+
assert result.data == {"test": 'maybe_string is "Cool"'}
67+
68+
result = graphql(schema, "{ test(maybe_string: null) }")
69+
assert not result.errors
70+
assert result.data == {"test": 'maybe_string is "null"'}
71+
72+
5273
def test_default_function_accesses_properties():
5374
# type: () -> None
5475
schema = _test_schema(GraphQLField(GraphQLString))

graphql/execution/values.py

Lines changed: 19 additions & 16 deletions
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
@@ -42,7 +43,7 @@ def get_variable_values(
4243
if inputs is None:
4344
inputs = {}
4445

45-
values = {}
46+
values = {} # type: Dict[str, Any]
4647
for def_ast in definition_asts:
4748
var_name = def_ast.variable.name.value
4849
var_type = type_from_ast(schema, def_ast.type)
@@ -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(
@@ -135,15 +137,16 @@ def get_argument_values(
135137
continue
136138

137139
else:
138-
value = value_from_ast(arg_ast.value, arg_type, variables) # type: ignore
139-
if value is None:
140-
if arg_def.default_value is not None:
141-
value = arg_def.default_value
142-
result[arg_def.out_name or name] = value
140+
arg_name = arg_def.out_name or name # type: ignore
141+
arg_ast_value = arg_ast.value # type: ignore
142+
value = value_from_ast(arg_ast_value, arg_type, variables) # type: ignore
143+
if value is None and not isinstance(arg_ast_value, ast.NullValue):
144+
if arg_def.default_value is not Undefined:
145+
result[arg_name] = arg_def.default_value
146+
else:
147+
result[arg_name] = None
143148
else:
144-
# We use out_name as the output name for the
145-
# dict if exists
146-
result[arg_def.out_name or name] = value
149+
result[arg_name] = value
147150

148151
return result
149152

@@ -172,7 +175,7 @@ def coerce_value(type, value):
172175
obj = {}
173176
for field_name, field in fields.items():
174177
if field_name not in value:
175-
if field.default_value is not None:
178+
if field.default_value is not Undefined:
176179
field_value = field.default_value
177180
obj[field.out_name or field_name] = field_value
178181
else:

graphql/language/ast.py

Lines changed: 42 additions & 0 deletions
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

Lines changed: 16 additions & 8 deletions
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,17 +500,18 @@ 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-
AEFC return ast.EnumValue(
505-
value=token.value, loc=loc(parser, token.start) # type: ignore
506-
)
509+
if token.value == "null":
510+
return ast.NullValue(loc=loc(parser, token.start)) # type: ignore
511+
512+
return ast.EnumValue(
513+
value=token.value, loc=loc(parser, token.start) # type: ignore
514+
)
507515

508516
elif token.kind == TokenKind.DOLLAR:
509517
if not is_const:
@@ -754,7 +762,7 @@ def parse_input_value_def(parser):
754762
type=expect(parser, TokenKind.COLON) and parse_type(parser), # type: ignore
755763
default_value=parse_const_value(parser)
756764
if skip(parser, TokenKind.EQUALS)
757-
else None,
765+
else Undefined,
758766
directives=parse_directives(parser),
759767
loc=loc(parser, start),
760768
)

0 commit comments

Comments
 (0)
0