Skip to content

Commit

Permalink
Merge pull request #98 from melinath/57-non-null-fields
Browse files Browse the repository at this point in the history
57 non null fields
  • Loading branch information
melinath committed Mar 12, 2018
2 parents 0013e46 + 228238f commit c1a4759
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 17 deletions.
11 changes: 7 additions & 4 deletions django_graph_api/graphql/schema.py
Expand Up @@ -24,6 +24,7 @@
SCALAR,
String,
UNION,
NonNull,
)
from django_graph_api.graphql.utils import (
format_error,
Expand Down Expand Up @@ -174,6 +175,10 @@ def get_type(self):
type_ = field.object_type
if isinstance(field.type_, List):
type_ = List(type_)
elif not field.null:
type_ = NonNull(type_)
elif not field.null:
type_ = NonNull(field.type_)
else:
type_ = field.type_
return type_
Expand Down Expand Up @@ -204,7 +209,7 @@ class TypeObject(Object):
ofType = RelatedField('self')

def get_name(self):
if self.data.kind == LIST:
if self.data.kind in [LIST, NON_NULL]:
return None
return self.data.object_name

Expand Down Expand Up @@ -241,7 +246,7 @@ def get_enumValues(self):
return self.data.values

def get_ofType(self):
if self.data.kind == LIST:
if self.data.kind in [LIST, NON_NULL]:
return self.data.type_
return None

Expand Down Expand Up @@ -269,8 +274,6 @@ def _collect_types(self, object_type, types=None):
if enum_type in types:
continue
types.add(enum_type)
elif isinstance(field.type_, List):
field = field.type_
elif field.type_:
types.add(field.type_)
return types
Expand Down
26 changes: 22 additions & 4 deletions django_graph_api/graphql/types.py
Expand Up @@ -52,9 +52,10 @@ class Field(object):
creation_counter = 0
arguments = {}

def __init__(self, description=None, arguments=None):
def __init__(self, description=None, arguments=None, null=True):
self.arguments = arguments or {}
self.description = description
self.null = null

# Increase the creation counter, and save our local copy.
self.creation_counter = Field.creation_counter
Expand All @@ -66,11 +67,17 @@ def __init__(self, description=None, arguments=None):

def get_value(self):
raw_value = self.get_raw_value()
if not self.null and raw_value is None:
raise GraphQLError('Field {} returned null but is not nullable'.format(self.name))
if hasattr(self.type_, 'coerce_result'):
try:
return self.type_.coerce_result(raw_value)
except ValueError:
return None
raise GraphQLError('Cannot coerce {} ({}) to {}'.format(
type(raw_value).__name__,
raw_value,
self.type_.object_name
))
return raw_value

def get_raw_value(self):
Expand Down Expand Up @@ -106,8 +113,12 @@ def get_resolver_args(self):
input_value = self.obj.variables.get(variable_name, default_value)
try:
arg_value = value.coerce_input(input_value)
except TypeError:
error = 'Argument {} expected a {} but got a {}'.format(key, type(value), type(input_value))
except ValueError:
error = 'Query error: Argument {} expected a {} but got a {}'.format(
key,
type(value),
type(input_value)
)
raise GraphQLError(error)
resolver_args[key] = arg_value
else:
Expand Down Expand Up @@ -210,6 +221,13 @@ class Enum(Scalar):
values = ()


class NonNull(object):
kind = NON_NULL

def __init__(self, type_):
self.type_ = type_


class List(object):
kind = LIST

Expand Down
43 changes: 42 additions & 1 deletion django_graph_api/tests/graphql/test_types.py
Expand Up @@ -12,7 +12,10 @@
Float,
Int,
List,
String)
String,
IntegerField,
)
from django_graph_api.graphql.utils import GraphQLError


@patch('django_graph_api.graphql.types.Boolean.coerce_result')
Expand Down Expand Up @@ -43,6 +46,44 @@ def test_field_get_resolver_args_calls_coerce_input(coerce_input_mock):
coerce_input_mock.assert_called_once_with('bar')


def test_get_value_coercion_error():
field = IntegerField()
selection = mock.MagicMock()
selection.name = 'foo'
obj = mock.MagicMock()
obj.get_foo.return_value = 'bar'
field.bind(selection, obj)

with pytest.raises(GraphQLError):
field.get_value()


def test_get_resolver_args_coercion_error():
field = CharField(arguments={'foo': Int()})
selection = mock.MagicMock()
argument = mock.Mock()
argument.name = 'foo'
argument.value = 'bar'
selection.arguments = [argument]
obj = mock.MagicMock()
field.bind(selection, obj)

with pytest.raises(GraphQLError):
field.get_resolver_args()


def test_get_value_non_null():
field = IntegerField(null=False)
selection = mock.MagicMock()
selection.name = 'foo'
obj = mock.MagicMock()
obj.get_foo.return_value = None
field.bind(selection, obj)

with pytest.raises(GraphQLError):
field.get_value()


def test_int_coerce_result():
assert Int.coerce_result(True) is 1
assert Int.coerce_result(4.9) is 4
Expand Down
93 changes: 88 additions & 5 deletions django_graph_api/tests/test_introspection.py
Expand Up @@ -18,6 +18,7 @@
ManyRelatedField,
RelatedField,
String,
NonNull,
)

from test_app.schema import (
Expand Down Expand Up @@ -79,6 +80,36 @@ def test_type__scalar__get_ofType():
assert type_object.get_ofType() is None


def test_type__non_null__get_fields():
type_object = TypeObject(None, NonNull(Boolean), None)
assert type_object.get_fields() is None


def test_type__non_null__get_inputFields():
type_object = TypeObject(None, NonNull(Boolean), None)
assert type_object.get_inputFields() is None


def test_type__non_null__get_interfaces():
type_object = TypeObject(None, NonNull(Boolean), None)
assert type_object.get_interfaces() is None


def test_type__non_null__get_possibleTypes():
type_object = TypeObject(None, NonNull(Boolean), None)
assert type_object.get_possibleTypes() is None


def test_type__non_null__get_enumValues():
type_object = TypeObject(None, NonNull(Boolean), None)
assert type_object.get_enumValues() is None


def test_type__non_null__get_ofType():
type_object = TypeObject(None, NonNull(Boolean), None)
assert type_object.get_ofType() is Boolean


def test_type__object__get_name():
type_object = TypeObject(None, Character, None)
assert type_object.get_name() == 'Character'
Expand Down Expand Up @@ -301,6 +332,14 @@ def test_execute__filter_type():
kind
fields {
name
type {
name
kind
ofType {
name
kind
}
}
}
}
}
Expand All @@ -311,11 +350,55 @@ def test_execute__filter_type():
'name': 'Character',
'kind': 'OBJECT',
'fields': [
{'name': 'appears_in'},
{'name': 'best_friend'},
{'name': 'friends'},
{'name': 'id'},
{'name': 'name'},
{
'name': 'appears_in',
'type': {
'name': None,
'kind': 'LIST',
'ofType': {
'name': 'Episode',
'kind': 'OBJECT'
}
},
},
{
'name': 'best_friend',
'type': {
'name': 'Character',
'kind': 'OBJECT',
'ofType': None
},
},
{
'name': 'friends',
'type': {
'name': None,
'kind': 'LIST',
'ofType': {
'name': 'Character',
'kind': 'OBJECT'
}
},
},
{
'name': 'id',
'type': {
'name': None,
'kind': 'NON_NULL',
'ofType': {
'name': 'Int',
'kind': 'SCALAR'
}
},
},
{
'name': 'name',
'type': {
'name': 'String',
'kind': 'SCALAR',
'ofType': None
},
},
],
}
}
Expand Down
6 changes: 3 additions & 3 deletions test_app/schema.py
Expand Up @@ -46,7 +46,7 @@ def get_characters(self, types):


class Character(Object):
id = IntegerField()
id = IntegerField(null=False)
name = CharField()
friends = ManyRelatedField('self')
best_friend = RelatedField('self')
Expand All @@ -58,9 +58,9 @@ def get_best_friend(self):

@schema.register_query_root
class QueryRoot(Object):
hero = RelatedField(Character)
hero = RelatedField(Character, null=False)
episodes = ManyRelatedField(Episode)
episode = RelatedField(Episode, arguments={'number': Int()})
episode = RelatedField(Episode, arguments={'number': Int()}, null=False)

def get_hero(self):
return CharacterModel.objects.get(name='R2-D2')
Expand Down

0 comments on commit c1a4759

Please sign in to comment.