First working version with relay 💪

This commit is contained in:
Syrus Akbary 2015-09-25 23:25:10 -07:00
parent d2dc25cc07
commit 1b7caac39b
13 changed files with 441 additions and 56 deletions

View File

@ -1,7 +1,6 @@
from graphql.core.type import ( from graphql.core.type import (
GraphQLEnumType as Enum, GraphQLEnumType as Enum,
GraphQLArgument as Argument, GraphQLArgument as Argument,
# GraphQLSchema as Schema,
GraphQLString as String, GraphQLString as String,
GraphQLInt as Int, GraphQLInt as Int,
GraphQLID as ID GraphQLID as ID
@ -26,7 +25,3 @@ from graphene.core.types import (
from graphene.decorators import ( from graphene.decorators import (
resolve_only_args resolve_only_args
) )
from graphene.relay import (
Relay
)

View File

@ -99,6 +99,12 @@ class Field(object):
return '<%s>' % path 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): class TypeField(Field):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TypeField, self).__init__(self.field_type, *args, **kwargs) super(TypeField, self).__init__(self.field_type, *args, **kwargs)

View File

@ -54,7 +54,6 @@ class Options(object):
def add_field(self, field): def add_field(self, field):
self.local_fields.append(field) self.local_fields.append(field)
setattr(self.parent, field.field_name, field)
@cached_property @cached_property
def fields(self): def fields(self):

View File

@ -91,6 +91,10 @@ class ObjectType(six.with_metaclass(ObjectTypeMeta)):
self.instance = instance self.instance = instance
signals.post_init.send(self.__class__, instance=self) 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): def get_field(self, field):
return getattr(self.instance, field, None) return getattr(self.instance, field, None)

View File

@ -7,18 +7,24 @@ registered_object_types = []
def get_object_type(field_type, object_type=None): def get_object_type(field_type, object_type=None):
_is_class = inspect.isclass(field_type) native_type = get_type(field_type, object_type)
if _is_class and issubclass(field_type, ObjectType): if native_type:
field_type = field_type._meta.type field_type = native_type._meta.type
elif isinstance(field_type, basestring):
if field_type == 'self':
field_type = object_type._meta.type
else:
object_type = get_registered_object_type(field_type, object_type)
field_type = object_type._meta.type
return field_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):
return field_type
elif isinstance(field_type, basestring):
if field_type == 'self':
return object_type
else:
object_type = get_registered_object_type(field_type, object_type)
return object_type
return None
def get_registered_object_type(name, object_type=None): def get_registered_object_type(name, object_type=None):
app_label = None app_label = None
object_type_name = name object_type_name = name

View File

@ -1,2 +1,85 @@
class Relay(object): import collections
pass
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)

View File

@ -29,7 +29,7 @@ def test_interface():
assert Character._meta.type_name == 'Character' assert Character._meta.type_name == 'Character'
assert isinstance(object_type, GraphQLInterfaceType) assert isinstance(object_type, GraphQLInterfaceType)
assert object_type.description == 'Character description' 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(): def test_object_type():
object_type = Human._meta.type object_type = Human._meta.type
@ -37,5 +37,5 @@ def test_object_type():
assert Human._meta.type_name == 'Human' assert Human._meta.type_name == 'Human'
assert isinstance(object_type, GraphQLObjectType) assert isinstance(object_type, GraphQLObjectType)
assert object_type.description == 'Human description' 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] assert object_type.get_interfaces() == [Character._meta.type]

View File

View File

@ -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

View File

@ -2,60 +2,52 @@ import graphene
from graphene import resolve_only_args, relay from graphene import resolve_only_args, relay
from .data import ( from .data import (
getHero, getHuman, getCharacter, getDroid, getFaction,
Human as _Human, Droid as _Droid) getShip,
getRebels,
Episode = graphene.Enum('Episode', dict( getEmpire,
NEWHOPE=4, )
EMPIRE=5,
JEDI=6
))
def wrap_character(character): class Ship(relay.Node):
if isinstance(character, _Human): '''A ship in the Star Wars saga'''
return Human(character) name = graphene.StringField(description='The name of the ship.')
elif isinstance(character, _Droid):
return Droid(character) @classmethod
def get_node(cls, id):
ship = getShip(id)
if ship:
return Ship(ship)
class Character(graphene.Interface): class Faction(relay.Node):
name = graphene.StringField() '''A faction in the Star Wars saga'''
friends = relay.Connection('Character') name = graphene.StringField(description='The name of the faction.')
appearsIn = graphene.ListField(Episode) ships = relay.ConnectionField(Ship, description='The ships used by the faction.')
def resolve_friends(self, args, *_): @resolve_only_args
return [wrap_character(getCharacter(f)) for f in self.instance.friends] def resolve_ships(self, **kwargs):
return [Ship(getShip(ship)) for ship in self.instance.ships]
@classmethod
class Human(relay.Node, Character): def get_node(cls, id):
homePlanet = graphene.StringField() faction = getFaction(id)
if faction:
return Faction(faction)
class Droid(relay.Node, Character):
primaryFunction = graphene.StringField()
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
hero = graphene.Field(Character, rebels = graphene.Field(Faction)
episode=graphene.Argument(Episode)) empire = graphene.Field(Faction)
human = graphene.Field(Human, node = relay.NodeField()
id=graphene.Argument(graphene.String))
droid = graphene.Field(Droid,
id=graphene.Argument(graphene.String))
node = graphene.Field(relay.Node)
@resolve_only_args @resolve_only_args
def resolve_hero(self, episode): def resolve_rebels(self):
return wrap_character(getHero(episode)) return Faction(getRebels())
@resolve_only_args @resolve_only_args
def resolve_human(self, id): def resolve_empire(self):
return wrap_character(getHuman(id)) return Faction(getEmpire())
@resolve_only_args
def resolve_droid(self, id):
return wrap_character(getDroid(id))
Schema = graphene.Schema(query=Query) Schema = graphene.Schema(query=Query)

View File

@ -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)

View File

@ -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

View File

@ -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