From 266dd5efe089e92d235802166471b1fbbf7f58d6 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 17 Nov 2015 17:25:18 -0800 Subject: [PATCH 1/5] Fixed relay connections/edges field mapping. Improved testing --- graphene/contrib/django/fields.py | 12 +++++++----- graphene/relay/fields.py | 9 +++++---- graphene/relay/tests/test_types.py | 13 +++++++++++++ graphene/relay/types.py | 15 +++++---------- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 8cc7838e..5a4d95ae 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -11,7 +11,7 @@ class DjangoConnectionField(ConnectionField): def wrap_resolved(self, value, instance, args, info): schema = info.schema.graphene_schema - return lazy_map(value, self.type.get_object_type(schema)) + return lazy_map(value, self.type) class LazyListField(Field): @@ -22,7 +22,7 @@ class LazyListField(Field): def resolver(self, instance, args, info): schema = info.schema.graphene_schema resolved = super(LazyListField, self).resolver(instance, args, info) - return lazy_map(resolved, self.get_object_type(schema)) + return lazy_map(resolved, self.type) class ConnectionOrListField(Field): @@ -30,12 +30,14 @@ class ConnectionOrListField(Field): def internal_type(self, schema): model_field = self.type field_object_type = model_field.get_object_type(schema) + if not field_object_type: + raise SkipField() if is_node(field_object_type): - field = DjangoConnectionField(model_field) + field = DjangoConnectionField(field_object_type) else: - field = LazyListField(model_field) + field = LazyListField(field_object_type) field.contribute_to_class(self.object_type, self.name) - return field.internal_type(schema) + return schema.T(field) class DjangoModelField(FieldType): diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index 285d5a69..b3f6d328 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -10,12 +10,12 @@ from ..core.types.scalars import ID, Int, String class ConnectionField(Field): - def __init__(self, field_type, resolver=None, description='', + def __init__(self, type, resolver=None, description='', connection_type=None, edge_type=None, **kwargs): super( ConnectionField, self).__init__( - field_type, + type, resolver=resolver, before=String(), after=String(), @@ -38,7 +38,6 @@ class ConnectionField(Field): resolved = self.wrap_resolved(resolved, instance, args, info) assert isinstance( resolved, Iterable), 'Resolved value from the connection field have to be iterable' - type = schema.T(self.type) node = schema.objecttype(type) connection_type = self.get_connection_type(node) @@ -56,7 +55,8 @@ class ConnectionField(Field): return connection_type.for_node(node, edge_type=edge_type) def get_edge_type(self, node): - return self.edge_type or node.get_edge_type() + edge_type = self.edge_type or node.get_edge_type() + return edge_type.for_node(node) def get_type(self, schema): from graphene.relay.utils import is_node @@ -65,6 +65,7 @@ class ConnectionField(Field): assert is_node(node), 'Only nodes have connections.' schema.register(node) connection_type = self.get_connection_type(node) + return connection_type diff --git a/graphene/relay/tests/test_types.py b/graphene/relay/tests/test_types.py index da19eec4..1f3c56bf 100644 --- a/graphene/relay/tests/test_types.py +++ b/graphene/relay/tests/test_types.py @@ -1,4 +1,5 @@ from pytest import raises +from graphql.core.type import GraphQLList import graphene from graphene import relay @@ -31,3 +32,15 @@ def test_node_should_have_same_connection_always(): def test_node_should_have_id_field(): assert 'id' in OtherNode._meta.fields_map + + +def test_node_connection_should_have_edge(): + connection = relay.Connection.for_node(OtherNode) + edge = relay.Edge.for_node(OtherNode) + connection_type = schema.T(connection) + connection_fields = connection_type.get_fields() + assert 'edges' in connection_fields + assert 'pageInfo' in connection_fields + edges_type = connection_fields['edges'].type + assert isinstance(edges_type, GraphQLList) + assert edges_type.of_type == schema.T(edge) diff --git a/graphene/relay/types.py b/graphene/relay/types.py index 5b72e843..ec07b5c5 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -24,11 +24,6 @@ class PageInfo(ObjectType): class Edge(ObjectType): '''An edge in a connection.''' - class Meta: - type_name = 'DefaultEdge' - - node = Field(LazyType(lambda object_type: object_type.node_type), - description='The item at the end of the edge') cursor = String( required=True, description='A cursor for use in pagination') @@ -37,10 +32,11 @@ class Edge(ObjectType): def for_node(cls, node): from graphene.relay.utils import is_node assert is_node(node), 'ObjectTypes in a edge have to be Nodes' + node_field = Field(node, description='The item at the end of the edge') return type( '%s%s' % (node._meta.type_name, cls._meta.type_name), (cls,), - {'node_type': node}) + {'node_type': node, 'node': node_field}) class Connection(ObjectType): @@ -50,8 +46,6 @@ class Connection(ObjectType): page_info = Field(PageInfo, required=True, description='The Information to aid in pagination') - edges = List(LazyType(lambda object_type: object_type.edge_type), - description='Information to aid in pagination.') _connection_data = None @@ -59,12 +53,13 @@ class Connection(ObjectType): @memoize def for_node(cls, node, edge_type=None): from graphene.relay.utils import is_node - edge_type = edge_type or Edge + edge_type = edge_type or Edge.for_node(node) assert is_node(node), 'ObjectTypes in a connection have to be Nodes' + edges = List(edge_type, description='Information to aid in pagination.') return type( '%s%s' % (node._meta.type_name, cls._meta.type_name), (cls,), - {'edge_type': edge_type.for_node(node)}) + {'edge_type': edge_type, 'edges': edges}) def set_connection_data(self, data): self._connection_data = data From 4ab8e4e7c683d3d7148922f9e4d9cde2af293f11 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 17 Nov 2015 17:29:35 -0800 Subject: [PATCH 2/5] Updated version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0219fc57..77b2e296 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ class PyTest(TestCommand): setup( name='graphene', - version='0.4.0', + version='0.4.0.1', description='Graphene: Python DSL for GraphQL', long_description=open('README.rst').read(), From a970d99b69ef01401b425c07d8f42d88f998bf85 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 17 Nov 2015 17:35:19 -0800 Subject: [PATCH 3/5] Fixed flake8 syntax --- graphene/contrib/django/fields.py | 2 -- graphene/relay/types.py | 1 - 2 files changed, 3 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 5a4d95ae..62d376d7 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -10,7 +10,6 @@ from .utils import get_type_for_model, lazy_map class DjangoConnectionField(ConnectionField): def wrap_resolved(self, value, instance, args, info): - schema = info.schema.graphene_schema return lazy_map(value, self.type) @@ -20,7 +19,6 @@ class LazyListField(Field): return List(self.type) def resolver(self, instance, args, info): - schema = info.schema.graphene_schema resolved = super(LazyListField, self).resolver(instance, args, info) return lazy_map(resolved, self.type) diff --git a/graphene/relay/types.py b/graphene/relay/types.py index ec07b5c5..08df4869 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -3,7 +3,6 @@ from graphql_relay.node.node import to_global_id from ..core.types import (Boolean, Field, InputObjectType, Interface, List, Mutation, ObjectType, String) from ..core.types.argument import ArgumentsGroup -from ..core.types.base import LazyType from ..core.types.definitions import NonNull from ..utils import memoize from .fields import GlobalIDField From a79a76d3b952c8d29a543eae1a320d6568c79350 Mon Sep 17 00:00:00 2001 From: Jon Rosebaugh Date: Tue, 17 Nov 2015 23:43:20 -0500 Subject: [PATCH 4/5] Add the info parameter (ResolveInfo) to get_node() calls. This allows request_context (or any other ResolveInfo data) to be used while getting nodes. For example, some data might need to be hidden based on the user's authorization; you would use info.request_context for this. Fixes #34. --- examples/starwars_django/schema.py | 4 +-- examples/starwars_relay/schema.py | 4 +-- graphene/contrib/django/tests/test_query.py | 4 +-- graphene/relay/fields.py | 2 +- graphene/relay/tests/test_query.py | 35 ++++++++++++++++++++- 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/examples/starwars_django/schema.py b/examples/starwars_django/schema.py index 7f267fed..501ccce6 100644 --- a/examples/starwars_django/schema.py +++ b/examples/starwars_django/schema.py @@ -17,7 +17,7 @@ class Ship(DjangoNode): model = ShipModel @classmethod - def get_node(cls, id): + def get_node(cls, id, info): return Ship(get_ship(id)) @@ -33,7 +33,7 @@ class Faction(DjangoNode): model = FactionModel @classmethod - def get_node(cls, id): + def get_node(cls, id, info): return Faction(get_faction(id)) diff --git a/examples/starwars_relay/schema.py b/examples/starwars_relay/schema.py index da31a6f7..f2c33834 100644 --- a/examples/starwars_relay/schema.py +++ b/examples/starwars_relay/schema.py @@ -11,7 +11,7 @@ class Ship(relay.Node): name = graphene.String(description='The name of the ship.') @classmethod - def get_node(cls, id): + def get_node(cls, id, info): return get_ship(id) @@ -27,7 +27,7 @@ class Faction(relay.Node): return [get_ship(ship_id) for ship_id in self.ships] @classmethod - def get_node(cls, id): + def get_node(cls, id, info): return get_faction(id) diff --git a/graphene/contrib/django/tests/test_query.py b/graphene/contrib/django/tests/test_query.py index a8e8229b..090c8695 100644 --- a/graphene/contrib/django/tests/test_query.py +++ b/graphene/contrib/django/tests/test_query.py @@ -66,7 +66,7 @@ def test_should_node(): model = Reporter @classmethod - def get_node(cls, id): + def get_node(cls, id, info): return ReporterNode(Reporter(id=2, first_name='Cookie Monster')) def resolve_articles(self, *args, **kwargs): @@ -78,7 +78,7 @@ def test_should_node(): model = Article @classmethod - def get_node(cls, id): + def get_node(cls, id, info): return ArticleNode(Article(id=1, headline='Article node')) class Query(graphene.ObjectType): diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index b3f6d328..58e0e8bd 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -92,7 +92,7 @@ class NodeField(Field): object_type != self.field_object_type): return - return object_type.get_node(_id) + return object_type.get_node(_id, info) def resolver(self, instance, args, info): global_id = args.get('id') diff --git a/graphene/relay/tests/test_query.py b/graphene/relay/tests/test_query.py index 2dec950c..cfe5502e 100644 --- a/graphene/relay/tests/test_query.py +++ b/graphene/relay/tests/test_query.py @@ -1,3 +1,4 @@ +import pytest from graphql.core.type import GraphQLID, GraphQLNonNull import graphene @@ -15,12 +16,22 @@ class MyNode(relay.Node): name = graphene.String() @classmethod - def get_node(cls, id): + def get_node(cls, id, info): return MyNode(id=id, name='mo') +class SpecialNode(relay.Node): + value = graphene.String() + + @classmethod + def get_node(cls, id, info): + value = "!!!" if info.request_context.get('is_special') else "???" + return SpecialNode(id=id, value=value) + + class Query(graphene.ObjectType): my_node = relay.NodeField(MyNode) + special_node = relay.NodeField(SpecialNode) all_my_nodes = relay.ConnectionField( MyNode, connection_type=MyConnection, customArg=graphene.String()) @@ -79,6 +90,28 @@ def test_nodefield_query(): assert result.data == expected +@pytest.mark.parametrize('specialness,value', [(True, '!!!'), (False, '???')]) +def test_get_node_info(specialness, value): + query = ''' + query ValueQuery { + specialNode(id:"U3BlY2lhbE5vZGU6Mg==") { + id + value + } + } + ''' + + expected = { + 'specialNode': { + 'id': 'U3BlY2lhbE5vZGU6Mg==', + 'value': value, + }, + } + result = schema.execute(query, request_context={'is_special': specialness}) + assert not result.errors + assert result.data == expected + + def test_nodeidfield(): id_field = MyNode._meta.fields_map['id'] id_field_type = schema.T(id_field) From fca0ab4f4cb9d2d32f6cd827b676ede62351c45b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 18 Nov 2015 17:53:11 -0800 Subject: [PATCH 5/5] Added schema str representation --- graphene/core/schema.py | 4 ++++ graphene/core/tests/test_schema.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index d3c728d1..a175b59d 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -6,6 +6,7 @@ from graphql.core.execution.middlewares.sync import \ SynchronousExecutionMiddleware from graphql.core.type import GraphQLSchema as _GraphQLSchema from graphql.core.utils.introspection_query import introspection_query +from graphql.core.utils.schema_printer import print_schema from graphene import signals @@ -89,6 +90,9 @@ class Schema(object): objecttype) and issubclass(objecttype, BaseObjectType): return objecttype + def __str__(self): + return print_schema(self.schema) + def setup(self): assert self.query, 'The base query type is not set' self.T(self.query) diff --git a/graphene/core/tests/test_schema.py b/graphene/core/tests/test_schema.py index 5dcf68a4..189111a1 100644 --- a/graphene/core/tests/test_schema.py +++ b/graphene/core/tests/test_schema.py @@ -152,3 +152,22 @@ def test_lazytype(): schema.query = MyType assert schema.T(t) == schema.T(MyType) + + +def test_schema_str(): + expected = """ +interface Character { + name: String +} + +type Human implements Character { + name: String + friends: [Character] + pet: Pet +} + +type Pet { + type: String +} +""".lstrip() + assert str(schema) == expected