mirror of
https://github.com/graphql-python/graphene.git
synced 2024-11-29 13:03:56 +03:00
First working version with relay 💪
This commit is contained in:
parent
d2dc25cc07
commit
1b7caac39b
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -7,18 +7,24 @@ registered_object_types = []
|
|||
|
||||
|
||||
def get_object_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
|
||||
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
|
||||
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):
|
||||
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):
|
||||
app_label = None
|
||||
object_type_name = name
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
0
tests/starwars_relay/__init__.py
Normal file
0
tests/starwars_relay/__init__.py
Normal file
98
tests/starwars_relay/data.py
Normal file
98
tests/starwars_relay/data.py
Normal 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
|
|
@ -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)
|
||||
|
|
60
tests/starwars_relay/schema_other.py
Normal file
60
tests/starwars_relay/schema_other.py
Normal 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)
|
37
tests/starwars_relay/test_connections.py
Normal file
37
tests/starwars_relay/test_connections.py
Normal 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
|
105
tests/starwars_relay/test_objectidentification.py
Normal file
105
tests/starwars_relay/test_objectidentification.py
Normal 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
|
Loading…
Reference in New Issue
Block a user