Skip to content

Commit

Permalink
support null values
Browse files Browse the repository at this point in the history
- addresses graphql-python#118
- initial implementation by @yen223 in PR graphql-python#119
  • Loading branch information
jaemk authored and cpmsmith committed Apr 27, 2022
1 parent 016d975 commit 24f07c9
Show file tree
Hide file tree
Showing 32 changed files with 381 additions and 64 deletions.
3 changes: 3 additions & 0 deletions graphql/execution/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,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
18 changes: 10 additions & 8 deletions graphql/execution/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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 Down Expand Up @@ -56,10 +57,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 @@ -109,7 +111,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 @@ -123,7 +125,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 @@ -137,7 +139,7 @@ def get_argument_values(
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:
if arg_def.default_value is not Undefined:
value = arg_def.default_value
result[arg_def.out_name or name] = value
else:
Expand Down Expand Up @@ -172,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: 17 additions & 7 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 @@ -65,6 +66,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 @@ -338,7 +345,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 @@ -493,18 +500,21 @@ 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(
value=token.value == "true", loc=loc(parser, token.start)
)

if token.value != "null":
advance(parser)
return ast.EnumValue(
value=token.value, loc=loc(parser, token.start) # type: ignore
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:
return parse_variable(parser)
Expand Down Expand Up @@ -754,7 +764,7 @@ def parse_input_value_def(parser):
type=expect(parser, TokenKind.COLON) and parse_type(parser), # type: ignore
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
24 changes: 19 additions & 5 deletions graphql/language/printer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json

from .visitor import Visitor, visit
from ..utils.undefined import Undefined

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

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

def leave_SelectionSet(self, node, *args):
# type: (Any, *Any) -> str
Expand Down Expand Up @@ -111,6 +112,12 @@ def leave_BooleanValue(self, node, *args):
# type: (Any, *Any) -> str
return json.dumps(node.value)

def leave_NullValue(self, node, *args):
return "null"

def leave_UndefinedValue(self, node, *args):
return Undefined

def leave_EnumValue(self, node, *args):
# type: (Any, *Any) -> str
return node.value
Expand Down Expand Up @@ -192,7 +199,7 @@ def leave_InputValueDefinition(self, node, *args):
node.name
+ ": "
+ node.type
+ wrap(" = ", node.default_value)
+ wrap(" = ", node.default_value, is_default_value=True)
+ wrap(" ", join(node.directives, " "))
)

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

def leave_InputObjectTypeDefinition(self, node, *args):
# type: (Any, *Any) -> str
return (
s = (
"input "
+ node.name
+ wrap(" ", join(node.directives, " "))
+ " "
+ block(node.fields)
)
return s

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


def wrap(start, maybe_str, end=""):
# type: (str, Optional[str], str) -> str
def wrap(start, maybe_str, end="", is_default_value=False):
# type: (str, Optional[str], str, bool) -> str
if is_default_value:
if maybe_str is Undefined:
return ""
s = "null" if maybe_str is None else maybe_str
return start + s + end

if maybe_str:
return start + maybe_str + end
return ""
Expand Down
2 changes: 1 addition & 1 deletion graphql/language/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
}
{
unnamed(truthy: true, falsey: false),
unnamed(truthy: true, falsey: false, nullish: null),
query
}
"""
Expand Down
69 changes: 65 additions & 4 deletions graphql/language/tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,72 @@ def test_does_not_accept_fragments_spread_of_on():
assert "Syntax Error GraphQL (1:9) Expected Name, found }" in excinfo.value.message


def test_does_not_allow_null_value():
def test_allows_null_value():
# type: () -> None
with raises(GraphQLSyntaxError) as excinfo:
parse("{ fieldWithNullableStringInput(input: null) }")
parse("{ fieldWithNullableStringInput(input: null) }")


def test_parses_null_value_to_null():
result = parse('{ fieldWithObjectInput(input: {a: null, b: null, c: "C", d: null}) }')
values = result.definitions[0].selection_set.selections[0].arguments[0].value.fields
expected = (
(u"a", ast.NullValue()),
(u"b", ast.NullValue()),
(u"c", ast.StringValue(value=u"C")),
(u"d", ast.NullValue()),
)
for name_value, actual in zip(expected, values):
assert name_value == (actual.name.value, actual.value)


def test_parses_null_value_in_list():
result = parse('{ fieldWithObjectInput(input: {b: ["A", null, "C"], c: "C"}) }')
assert result == ast.Document(
definitions=[
ast.OperationDefinition(
operation="query", name=None, variable_definitions=None, directives=[],
selection_set=ast.SelectionSet(
selections=[
ast.Field(
alias=None,
name=ast.Name(value=u"fieldWithObjectInput"),
directives=[],
selection_set=None,
arguments=[
ast.Argument(
name=ast.Name(value=u"input"),
value=ast.ObjectValue(
fields=[
ast.ObjectField(
name=ast.Name(value=u"b"),
value=ast.ListValue(
values=[
ast.StringValue(value=u"A"),
ast.NullValue(),
ast.StringValue(value=u"C"),
],
),
),
ast.ObjectField(
name=ast.Name(value=u"c"),
value=ast.StringValue(value=u"C"),
),
]
),
),
],
),
],
),
),
],
)


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


def test_parses_multi_byte_characters():
Expand Down Expand Up @@ -158,6 +218,7 @@ def tesst_allows_non_keywords_anywhere_a_name_is_allowed():
"subscription",
"true",
"false",
"null",
]

query_template = """
Expand Down
10 changes: 9 additions & 1 deletion graphql/language/tests/test_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ def test_correctly_prints_mutation_with_artifacts():
)


def test_correctly_prints_null():
query_ast_shorthanded = parse('{ thingy(null: "wow", name: null) }')
assert print_ast(query_ast_shorthanded) == """{
thingy(null: "wow", name: null)
}
"""


def test_prints_kitchen_sink():
# type: () -> None
ast = parse(KITCHEN_SINK)
Expand Down Expand Up @@ -138,7 +146,7 @@ def test_prints_kitchen_sink():
}
{
unnamed(truthy: true, falsey: false)
unnamed(truthy: true, falsey: false, nullish: null)
query
}
"""
Expand Down
Loading

0 comments on commit 24f07c9

Please sign in to comment.