diff --git a/graphene/__init__.py b/graphene/__init__.py index 6024313c..c84eeb8c 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -1,7 +1,6 @@ from graphql.core.type import ( GraphQLEnumType as Enum, GraphQLArgument as Argument, - # GraphQLSchema as Schema, GraphQLString as String, GraphQLInt as Int, GraphQLID as ID @@ -26,7 +25,3 @@ from graphene.core.types import ( from graphene.decorators import ( resolve_only_args ) - -from graphene.relay import ( - Relay -) diff --git a/graphene/core/fields.py b/graphene/core/fields.py index d37fed1d..478e3630 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -99,6 +99,12 @@ class Field(object): return '<%s>' % path +class NativeField(Field): + def __init__(self, field=None): + super(NativeField, self).__init__(None) + self.field = field or getattr(self, 'field') + + class TypeField(Field): def __init__(self, *args, **kwargs): super(TypeField, self).__init__(self.field_type, *args, **kwargs) diff --git a/graphene/core/options.py b/graphene/core/options.py index a277be15..c3dc13af 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -54,7 +54,6 @@ class Options(object): def add_field(self, field): self.local_fields.append(field) - setattr(self.parent, field.field_name, field) @cached_property def fields(self): diff --git a/graphene/core/types.py b/graphene/core/types.py index e83591b1..3ffb466d 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -91,6 +91,10 @@ class ObjectType(six.with_metaclass(ObjectTypeMeta)): self.instance = instance signals.post_init.send(self.__class__, instance=self) + def __getattr__(self, name): + if self.instance: + return getattr(self.instance, name) + def get_field(self, field): return getattr(self.instance, field, None) diff --git a/graphene/core/utils.py b/graphene/core/utils.py index 69039d89..b50a7664 100644 --- a/graphene/core/utils.py +++ b/graphene/core/utils.py @@ -7,17 +7,23 @@ registered_object_types = [] def get_object_type(field_type, object_type=None): + native_type = get_type(field_type, object_type) + if native_type: + field_type = native_type._meta.type + return field_type + + +def get_type(field_type, object_type=None): _is_class = inspect.isclass(field_type) if _is_class and issubclass(field_type, ObjectType): - field_type = field_type._meta.type + return field_type elif isinstance(field_type, basestring): if field_type == 'self': - field_type = object_type._meta.type + return object_type else: object_type = get_registered_object_type(field_type, object_type) - field_type = object_type._meta.type - return field_type - + return object_type + return None def get_registered_object_type(name, object_type=None): app_label = None diff --git a/graphene/relay/__init__.py b/graphene/relay/__init__.py index 80feb122..d93aa7ac 100644 --- a/graphene/relay/__init__.py +++ b/graphene/relay/__init__.py @@ -1,2 +1,85 @@ -class Relay(object): - pass +import collections + +from graphene import signals +from graphene.core.fields import Field, NativeField +from graphene.core.types import Interface +from graphene.core.utils import get_type +from graphene.utils import cached_property + +from graphql_relay.node.node import ( + nodeDefinitions, + globalIdField, + fromGlobalId +) +from graphql_relay.connection.arrayconnection import ( + connectionFromArray +) +from graphql_relay.connection.connection import ( + connectionArgs, + connectionDefinitions +) + +registered_nodes = {} + + +def getNode(globalId, *args): + resolvedGlobalId = fromGlobalId(globalId) + _type, _id = resolvedGlobalId.type, resolvedGlobalId.id + if _type in registered_nodes: + object_type = registered_nodes[_type] + return object_type.get_node(_id) + + +def getNodeType(obj): + return obj._meta.type + + +_nodeDefinitions = nodeDefinitions(getNode, getNodeType) + + +class Node(Interface): + @classmethod + def get_graphql_type(cls): + if cls is Node: + # Return only nodeInterface when is the Node Inerface + return _nodeDefinitions.nodeInterface + return super(Node, cls).get_graphql_type() + + +class NodeField(NativeField): + field = _nodeDefinitions.nodeField + + +class ConnectionField(Field): + def __init__(self, field_type, resolve=None, description=''): + super(ConnectionField, self).__init__(field_type, resolve=resolve, + args=connectionArgs, description=description) + + def resolve(self, instance, args, info): + resolved = super(ConnectionField, self).resolve(instance, args, info) + if resolved: + assert isinstance(resolved, collections.Iterable), 'Resolved value from the connection field have to be iterable' + return connectionFromArray(resolved, args) + + @cached_property + def type(self): + object_type = get_type(self.field_type, self.object_type) + assert issubclass(object_type, Node), 'Only nodes have connections.' + return object_type.connection + + +@signals.class_prepared.connect +def object_type_created(object_type): + if issubclass(object_type, Node): + type_name = object_type._meta.type_name + assert type_name not in registered_nodes, 'Two nodes with the same type_name: %s' % type_name + registered_nodes[type_name] = object_type + # def getId(*args, **kwargs): + # print '**GET ID', args, kwargs + # return 2 + field = NativeField(globalIdField(type_name)) + object_type.add_to_class('id', field) + assert hasattr(object_type, 'get_node'), 'get_node classmethod not found in %s Node' % type_name + + connection = connectionDefinitions(type_name, object_type._meta.type).connectionType + object_type.add_to_class('connection', connection) diff --git a/tests/core/test_types.py b/tests/core/test_types.py index e8d3ec69..b17cae48 100644 --- a/tests/core/test_types.py +++ b/tests/core/test_types.py @@ -29,7 +29,7 @@ def test_interface(): assert Character._meta.type_name == 'Character' assert isinstance(object_type, GraphQLInterfaceType) assert object_type.description == 'Character description' - assert object_type.get_fields() == {'name': Character.name.field} + assert object_type.get_fields() == {'name': Character._meta.fields_map['name'].field} def test_object_type(): object_type = Human._meta.type @@ -37,5 +37,5 @@ def test_object_type(): assert Human._meta.type_name == 'Human' assert isinstance(object_type, GraphQLObjectType) assert object_type.description == 'Human description' - assert object_type.get_fields() == {'name': Character.name.field, 'friends': Human.friends.field} + assert object_type.get_fields() == {'name': Character._meta.fields_map['name'].field, 'friends': Human._meta.fields_map['friends'].field} assert object_type.get_interfaces() == [Character._meta.type] diff --git a/tests/starwars_relay/__init__.py b/tests/starwars_relay/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/starwars_relay/data.py b/tests/starwars_relay/data.py new file mode 100644 index 00000000..31ac591d --- /dev/null +++ b/tests/starwars_relay/data.py @@ -0,0 +1,98 @@ +from collections import namedtuple + +Ship = namedtuple('Ship',['id', 'name']) +Faction = namedtuple('Faction',['id', 'name', 'ships']) + +xwing = Ship( + id='1', + name='X-Wing', +) + +ywing = Ship( + id='2', + name='Y-Wing', +) + +awing = Ship( + id='3', + name='A-Wing', +) + +# Yeah, technically it's Corellian. But it flew in the service of the rebels, +# so for the purposes of this demo it's a rebel ship. +falcon = Ship( + id='4', + name='Millenium Falcon', +) + +homeOne = Ship( + id='5', + name='Home One', +) + +tieFighter = Ship( + id='6', + name='TIE Fighter', +) + +tieInterceptor = Ship( + id='7', + name='TIE Interceptor', +) + +executor = Ship( + id='8', + name='Executor', +) + +rebels = Faction( + id='1', + name='Alliance to Restore the Republic', + ships=['1', '2', '3', '4', '5'] +) + +empire = Faction( + id='2', + name='Galactic Empire', + ships= ['6', '7', '8'] +) + +data = { + 'Faction': { + '1': rebels, + '2': empire + }, + 'Ship': { + '1': xwing, + '2': ywing, + '3': awing, + '4': falcon, + '5': homeOne, + '6': tieFighter, + '7': tieInterceptor, + '8': executor + } +} + +def createShip(shipName, factionId): + nextShip = len(data['Ship'].keys())+1 + newShip = Ship( + id=str(nextShip), + name=shipName + ) + data['Ship'][newShip.id] = newShip + data['Faction'][factionId].ships.append(newShip.id) + return newShip + + +def getShip(_id): + return data['Ship'][_id] + +def getFaction(_id): + return data['Faction'][_id] + +def getRebels(): + return rebels + +def getEmpire(): + return empire diff --git a/tests/starwars_relay/schema.py b/tests/starwars_relay/schema.py index b2e65db5..55e03e6c 100644 --- a/tests/starwars_relay/schema.py +++ b/tests/starwars_relay/schema.py @@ -2,60 +2,52 @@ import graphene from graphene import resolve_only_args, relay from .data import ( - getHero, getHuman, getCharacter, getDroid, - Human as _Human, Droid as _Droid) - -Episode = graphene.Enum('Episode', dict( - NEWHOPE=4, - EMPIRE=5, - JEDI=6 -)) + getFaction, + getShip, + getRebels, + getEmpire, +) -def wrap_character(character): - if isinstance(character, _Human): - return Human(character) - elif isinstance(character, _Droid): - return Droid(character) +class Ship(relay.Node): + '''A ship in the Star Wars saga''' + name = graphene.StringField(description='The name of the ship.') + + @classmethod + def get_node(cls, id): + ship = getShip(id) + if ship: + return Ship(ship) -class Character(graphene.Interface): - name = graphene.StringField() - friends = relay.Connection('Character') - appearsIn = graphene.ListField(Episode) +class Faction(relay.Node): + '''A faction in the Star Wars saga''' + name = graphene.StringField(description='The name of the faction.') + ships = relay.ConnectionField(Ship, description='The ships used by the faction.') - def resolve_friends(self, args, *_): - return [wrap_character(getCharacter(f)) for f in self.instance.friends] + @resolve_only_args + def resolve_ships(self, **kwargs): + return [Ship(getShip(ship)) for ship in self.instance.ships] - -class Human(relay.Node, Character): - homePlanet = graphene.StringField() - - -class Droid(relay.Node, Character): - primaryFunction = graphene.StringField() + @classmethod + def get_node(cls, id): + faction = getFaction(id) + if faction: + return Faction(faction) class Query(graphene.ObjectType): - hero = graphene.Field(Character, - episode=graphene.Argument(Episode)) - human = graphene.Field(Human, - id=graphene.Argument(graphene.String)) - droid = graphene.Field(Droid, - id=graphene.Argument(graphene.String)) - node = graphene.Field(relay.Node) + rebels = graphene.Field(Faction) + empire = graphene.Field(Faction) + node = relay.NodeField() @resolve_only_args - def resolve_hero(self, episode): - return wrap_character(getHero(episode)) + def resolve_rebels(self): + return Faction(getRebels()) @resolve_only_args - def resolve_human(self, id): - return wrap_character(getHuman(id)) - - @resolve_only_args - def resolve_droid(self, id): - return wrap_character(getDroid(id)) + def resolve_empire(self): + return Faction(getEmpire()) Schema = graphene.Schema(query=Query) diff --git a/tests/starwars_relay/schema_other.py b/tests/starwars_relay/schema_other.py new file mode 100644 index 00000000..dd33bba7 --- /dev/null +++ b/tests/starwars_relay/schema_other.py @@ -0,0 +1,60 @@ +import graphene +from graphene import resolve_only_args, relay + +from .data import ( + getHero, getHuman, getCharacter, getDroid, + Human as _Human, Droid as _Droid) + +Episode = graphene.Enum('Episode', dict( + NEWHOPE=4, + EMPIRE=5, + JEDI=6 +)) + +def wrap_character(character): + if isinstance(character, _Human): + return Human(character) + elif isinstance(character, _Droid): + return Droid(character) + + +class Character(graphene.Interface): + name = graphene.StringField() + friends = relay.Connection('Character') + appearsIn = graphene.ListField(Episode) + + def resolve_friends(self, args, *_): + return [wrap_character(getCharacter(f)) for f in self.instance.friends] + + +class Human(relay.Node, Character): + homePlanet = graphene.StringField() + + +class Droid(relay.Node, Character): + primaryFunction = graphene.StringField() + + +class Query(graphene.ObjectType): + hero = graphene.Field(Character, + episode=graphene.Argument(Episode)) + human = graphene.Field(Human, + id=graphene.Argument(graphene.String)) + droid = graphene.Field(Droid, + id=graphene.Argument(graphene.String)) + node = relay.NodeField() + + @resolve_only_args + def resolve_hero(self, episode): + return wrap_character(getHero(episode)) + + @resolve_only_args + def resolve_human(self, id): + return wrap_character(getHuman(id)) + + @resolve_only_args + def resolve_droid(self, id): + return wrap_character(getDroid(id)) + + +Schema = graphene.Schema(query=Query) diff --git a/tests/starwars_relay/test_connections.py b/tests/starwars_relay/test_connections.py new file mode 100644 index 00000000..1bac0a4d --- /dev/null +++ b/tests/starwars_relay/test_connections.py @@ -0,0 +1,37 @@ +from pytest import raises +from graphql.core import graphql + +from .schema import Schema + +def test_correct_fetch_first_ship_rebels(): + query = ''' + query RebelsShipsQuery { + rebels { + name, + ships(first: 1) { + edges { + node { + name + } + } + } + } + } + ''' + expected = { + 'rebels': { + 'name': 'Alliance to Restore the Republic', + 'ships': { + 'edges': [ + { + 'node': { + 'name': 'X-Wing' + } + } + ] + } + } + } + result = Schema.execute(query) + assert result.errors == None + assert result.data == expected diff --git a/tests/starwars_relay/test_objectidentification.py b/tests/starwars_relay/test_objectidentification.py new file mode 100644 index 00000000..85050b6f --- /dev/null +++ b/tests/starwars_relay/test_objectidentification.py @@ -0,0 +1,105 @@ +from pytest import raises +from graphql.core import graphql + +from .schema import Schema + +def test_correctly_fetches_id_name_rebels(): + query = ''' + query RebelsQuery { + rebels { + id + name + } + } + ''' + expected = { + 'rebels': { + 'id': 'RmFjdGlvbjox', + 'name': 'Alliance to Restore the Republic' + } + } + result = Schema.execute(query) + assert result.errors == None + assert result.data == expected + +def test_correctly_refetches_rebels(): + query = ''' + query RebelsRefetchQuery { + node(id: "RmFjdGlvbjox") { + id + ... on Faction { + name + } + } + } + ''' + expected = { + 'node': { + 'id': 'RmFjdGlvbjox', + 'name': 'Alliance to Restore the Republic' + } + } + result = Schema.execute(query) + assert result.errors == None + assert result.data == expected + +def test_correctly_fetches_id_name_empire(): + query = ''' + query EmpireQuery { + empire { + id + name + } + } + ''' + expected = { + 'empire': { + 'id': 'RmFjdGlvbjoy', + 'name': 'Galactic Empire' + } + } + result = Schema.execute(query) + assert result.errors == None + assert result.data == expected + +def test_correctly_refetches_empire(): + query = ''' + query EmpireRefetchQuery { + node(id: "RmFjdGlvbjoy") { + id + ... on Faction { + name + } + } + } + ''' + expected = { + 'node': { + 'id': 'RmFjdGlvbjoy', + 'name': 'Galactic Empire' + } + } + result = Schema.execute(query) + assert result.errors == None + assert result.data == expected + +def test_correctly_refetches_xwing(): + query = ''' + query XWingRefetchQuery { + node(id: "U2hpcDox") { + id + ... on Ship { + name + } + } + } + ''' + expected = { + 'node': { + 'id': 'U2hpcDox', + 'name': 'X-Wing' + } + } + result = Schema.execute(query) + assert result.errors == None + assert result.data == expected