Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add nullvalue #172

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions graphql/execution/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,9 @@ def resolve_field(
executor = exe_context.executor
result = resolve_or_error(resolve_fn_middleware, source, info, args, executor)

if result is Undefined:
return Undefined

return complete_value_catching_error(
exe_context, return_type, field_asts, info, field_path, result
)
Expand Down
33 changes: 23 additions & 10 deletions graphql/execution/tests/test_execute_schema.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# type: ignore

from itertools import starmap, repeat
from typing import Union
from graphql.execution import execute
from graphql.language.parser import parse
from graphql.type import (
Expand Down Expand Up @@ -51,36 +53,44 @@ def test_executes_using_a_schema():
{
"id": GraphQLField(GraphQLNonNull(GraphQLString)),
"isPublished": GraphQLField(GraphQLBoolean),
"topic": GraphQLField(GraphQLString),
"author": GraphQLField(BlogAuthor),
"title": GraphQLField(GraphQLString),
"body": GraphQLField(GraphQLString),
"keywords": GraphQLField(GraphQLList(GraphQLString)),
},
)

def _resolve_article(obj, info, id, topic):
return Article(id, topic)

def _resolve_feed(*_):
return list(starmap(Article, zip(range(1, 10 + 1), repeat("food"))))

BlogQuery = GraphQLObjectType(
"Query",
{
"article": GraphQLField(
BlogArticle,
args={"id": GraphQLArgument(GraphQLID)},
resolver=lambda obj, info, **args: Article(args["id"]),
),
"feed": GraphQLField(
GraphQLList(BlogArticle),
resolver=lambda *_: map(Article, range(1, 10 + 1)),
args={
"id": GraphQLArgument(GraphQLID),
"topic": GraphQLArgument(GraphQLNonNull(GraphQLString)),
},
resolver=_resolve_article,
),
"feed": GraphQLField(GraphQLList(BlogArticle), resolver=_resolve_feed),
},
)

BlogSchema = GraphQLSchema(BlogQuery)

class Article(object):
def __init__(self, id):
# type: (int) -> None
def __init__(self, id, topic):
# type: (int, Union[str, None]) -> None
self.id = id
self.isPublished = True
self.author = Author()
self.topic = "My topic is {}".format(topic or "null")
self.title = "My Article {}".format(id)
self.body = "This is a post"
self.hidden = "This data is not exposed in the schema"
Expand All @@ -97,7 +107,7 @@ def pic(self, width, height):
@property
def recentArticle(self):
# type: () -> Article
return Article(1)
return Article(1, "food")

class Pic(object):
def __init__(self, uid, width, height):
Expand All @@ -112,7 +122,7 @@ def __init__(self, uid, width, height):
id,
title
},
article(id: "1") {
article(id: "1", topic: null) {
...articleFields,
author {
id,
Expand All @@ -132,6 +142,7 @@ def __init__(self, uid, width, height):
fragment articleFields on Article {
id,
isPublished,
topic,
title,
body,
hidden,
Expand Down Expand Up @@ -159,6 +170,7 @@ def __init__(self, uid, width, height):
"article": {
"id": "1",
"isPublished": True,
"topic": "My topic is null",
"title": "My Article 1",
"body": "This is a post",
"author": {
Expand All @@ -168,6 +180,7 @@ def __init__(self, uid, width, height):
"recentArticle": {
"id": "1",
"isPublished": True,
"topic": "My topic is food",
"title": "My Article 1",
"body": "This is a post",
"keywords": ["foo", "bar", "1", "true", None],
Expand Down
14 changes: 13 additions & 1 deletion graphql/execution/tests/test_mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,16 @@ def assert_evaluate_mutations_serially(executor=None):
},
fifth: immediatelyChangeTheNumber(newNumber: 5) {
theNumber
}
},
sixth: immediatelyChangeTheNumber(newNumber: null) {
theNumber
},
seventh: immediatelyChangeTheNumber(newNumber: 100) {
theNumber
},
eighth: immediatelyChangeTheNumber(newNumber: null) {
theNumber
},
}"""
ast = parse(doc)
result = execute(schema, ast, Root(6), operation_name="M", executor=executor)
Expand All @@ -118,6 +127,9 @@ def assert_evaluate_mutations_serially(executor=None):
"third": {"theNumber": 3},
"fourth": {"theNumber": 4},
"fifth": {"theNumber": 5},
"sixth": {"theNumber": None},
"seventh": {"theNumber": 100},
"eighth": {"theNumber": None},
}


Expand Down
21 changes: 21 additions & 0 deletions graphql/execution/tests/test_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ def _test_schema(test_field):
)


def test_explicit_null_is_passed_to_resolver():
def resolver(_, __, maybe_string):
return 'maybe_string is "{}"'.format(maybe_string or "null")

schema = _test_schema(
GraphQLField(
GraphQLString,
args=OrderedDict([("maybe_string", GraphQLArgument(GraphQLString))]),
resolver=resolver,
)
)

result = graphql(schema, '{ test(maybe_string: "Cool") }')
assert not result.errors
assert result.data == {"test": 'maybe_string is "Cool"'}

result = graphql(schema, "{ test(maybe_string: null) }")
assert not result.errors
assert result.data == {"test": 'maybe_string is "null"'}


def test_default_function_accesses_properties():
# type: () -> None
schema = _test_schema(GraphQLField(GraphQLString))
Expand Down
35 changes: 19 additions & 16 deletions graphql/execution/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ..utils.is_valid_value import is_valid_value
from ..utils.type_from_ast import type_from_ast
from ..utils.value_from_ast import value_from_ast
from ..utils.undefined import Undefined

# Necessary for static type checking
if False: # flake8: noqa
Expand All @@ -39,7 +40,7 @@ def get_variable_values(
if inputs is None:
inputs = {}

values = {}
values = {} # type: Dict[str, Any]
for def_ast in definition_asts:
var_name = def_ast.variable.name.value
var_type = type_from_ast(schema, def_ast.type)
Expand All @@ -53,10 +54,11 @@ def get_variable_values(
[def_ast],
)
elif value is None:
if def_ast.default_value is not None:
values[var_name] = value_from_ast(
def_ast.default_value, var_type
) # type: ignore
if def_ast.default_value is None:
values[var_name] = None
elif def_ast.default_value is not Undefined:
values[var_name] = value_from_ast(def_ast.default_value, var_type)

if isinstance(var_type, GraphQLNonNull):
raise GraphQLError(
'Variable "${var_name}" of required type "{var_type}" was not provided.'.format(
Expand Down Expand Up @@ -106,7 +108,7 @@ def get_argument_values(
arg_type = arg_def.type
arg_ast = arg_ast_map.get(name)
if name not in arg_ast_map:
if arg_def.default_value is not None:
if arg_def.default_value is not Undefined:
result[arg_def.out_name or name] = arg_def.default_value
continue
elif isinstance(arg_type, GraphQLNonNull):
Expand All @@ -120,7 +122,7 @@ def get_argument_values(
variable_name = arg_ast.value.name.value # type: ignore
if variables and variable_name in variables:
result[arg_def.out_name or name] = variables[variable_name]
elif arg_def.default_value is not None:
elif arg_def.default_value is not Undefined:
result[arg_def.out_name or name] = arg_def.default_value
elif isinstance(arg_type, GraphQLNonNull):
raise GraphQLError(
Expand All @@ -132,15 +134,16 @@ def get_argument_values(
continue

else:
value = value_from_ast(arg_ast.value, arg_type, variables) # type: ignore
if value is None:
if arg_def.default_value is not None:
value = arg_def.default_value
result[arg_def.out_name or name] = value
arg_name = arg_def.out_name or name # type: ignore
arg_ast_value = arg_ast.value # type: ignore
value = value_from_ast(arg_ast_value, arg_type, variables) # type: ignore
if value is None and not isinstance(arg_ast_value, ast.NullValue):
if arg_def.default_value is not Undefined:
result[arg_name] = arg_def.default_value
else:
result[arg_name] = None
else:
# We use out_name as the output name for the
# dict if exists
result[arg_def.out_name or name] = value
result[arg_name] = value

return result

Expand Down Expand Up @@ -171,7 +174,7 @@ def coerce_value(type, value):
obj = {}
for field_name, field in fields.items():
if field_name not in value:
if field.default_value is not None:
if field.default_value is not Undefined:
field_value = field.default_value
obj[field.out_name or field_name] = field_value
else:
Expand Down
42 changes: 42 additions & 0 deletions graphql/language/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,48 @@ def __hash__(self):
return id(self)


class NullValue(Value):
__slots__ = ("loc", "value")
_fields = ("value",)

def __init__(self, value=None, loc=None):
self.value = None
self.loc = loc

def __eq__(self, other):
return isinstance(other, NullValue)

def __repr__(self):
return "NullValue"

def __copy__(self):
return type(self)(self.value, self.loc)

def __hash__(self):
return id(self)


class UndefinedValue(Value):
__slots__ = ("loc", "value")
_fields = ("value",)

def __init__(self, value=None, loc=None):
self.value = None
self.loc = loc

def __eq__(self, other):
return isinstance(other, UndefinedValue)

def __repr__(self):
return "UndefinedValue"

def __copy__(self):
return type(self)(self.value, self.loc)

def __hash__(self):
return id(self)


class EnumValue(Value):
__slots__ = ("loc", "value")
_fields = ("value",)
Expand Down
24 changes: 16 additions & 8 deletions graphql/language/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from ..error import GraphQLSyntaxError
from .lexer import Lexer, TokenKind, get_token_desc, get_token_kind_desc
from .source import Source
from ..utils.undefined import Undefined

# Necessary for static type checking
if False: # flake8: noqa
Expand Down Expand Up @@ -67,6 +68,12 @@ def parse(source, **kwargs):


def parse_value(source, **kwargs):
if source is None:
return ast.NullValue()

if source is Undefined:
return ast.UndefinedValue()

options = {"no_location": False, "no_source": False}
options.update(kwargs)
source_obj = source
Expand Down Expand Up @@ -340,7 +347,7 @@ def parse_variable_definition(parser):
type=expect(parser, TokenKind.COLON) and parse_type(parser),
default_value=parse_value_literal(parser, True)
if skip(parser, TokenKind.EQUALS)
else None,
else Undefined,
loc=loc(parser, start),
)

Expand Down Expand Up @@ -495,17 +502,18 @@ def parse_value_literal(parser, is_const):
)

elif token.kind == TokenKind.NAME:
advance(parser)
if token.value in ("true", "false"):
advance(parser)
return ast.BooleanValue( # type: ignore
value=token.value == "true", loc=loc(parser, token.start)
)

if token.value != "null":
advance(parser)
return ast.EnumValue( # type: ignore
value=token.value, loc=loc(parser, token.start)
)
if token.value == "null":
return ast.NullValue(loc=loc(parser, token.start)) # type: ignore

return ast.EnumValue( # type: ignore
value=token.value, loc=loc(parser, token.start)
)

elif token.kind == TokenKind.DOLLAR:
if not is_const:
Expand Down Expand Up @@ -756,7 +764,7 @@ def parse_input_value_def(parser):
type=expect(parser, TokenKind.COLON) and parse_type(parser),
default_value=parse_const_value(parser)
if skip(parser, TokenKind.EQUALS)
else None,
else Undefined,
directives=parse_directives(parser),
loc=loc(parser, start),
)
Expand Down
Loading