mirror of
https://github.com/graphql-python/graphene.git
synced 2025-02-02 20:54:16 +03:00
commit
0037d8aa2e
|
@ -3,7 +3,7 @@ sudo: false
|
||||||
python:
|
python:
|
||||||
- 2.7
|
- 2.7
|
||||||
install:
|
install:
|
||||||
- pip install pytest pytest-cov coveralls flake8 six
|
- pip install pytest pytest-cov coveralls flake8 six blinker
|
||||||
- pip install git+https://github.com/dittos/graphqllib.git # Last version of graphqllib
|
- pip install git+https://github.com/dittos/graphqllib.git # Last version of graphqllib
|
||||||
- pip install graphql-relay
|
- pip install graphql-relay
|
||||||
- python setup.py develop
|
- python setup.py develop
|
||||||
|
|
56
README.md
56
README.md
|
@ -77,6 +77,62 @@ query = '''
|
||||||
result = Schema.execute(query)
|
result = Schema.execute(query)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Relay Schema
|
||||||
|
|
||||||
|
Graphene also supports Relay, check the (Starwars Relay example)[/tests/starwars_relay]!
|
||||||
|
|
||||||
|
```python
|
||||||
|
import graphene
|
||||||
|
from graphene import relay
|
||||||
|
|
||||||
|
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 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.')
|
||||||
|
|
||||||
|
@resolve_only_args
|
||||||
|
def resolve_ships(self, **kwargs):
|
||||||
|
return [Ship(getShip(ship)) for ship in self.instance.ships]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_node(cls, id):
|
||||||
|
faction = getFaction(id)
|
||||||
|
if faction:
|
||||||
|
return Faction(faction)
|
||||||
|
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
rebels = graphene.Field(Faction)
|
||||||
|
empire = graphene.Field(Faction)
|
||||||
|
node = relay.NodeField()
|
||||||
|
|
||||||
|
@resolve_only_args
|
||||||
|
def resolve_rebels(self):
|
||||||
|
return Faction(getRebels())
|
||||||
|
|
||||||
|
@resolve_only_args
|
||||||
|
def resolve_empire(self):
|
||||||
|
return Faction(getEmpire())
|
||||||
|
|
||||||
|
|
||||||
|
Schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
|
# Later on, for querying
|
||||||
|
Schema.execute('''rebels { name }''')
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
After cloning this repo, ensure dependencies are installed by running:
|
After cloning this repo, ensure dependencies are installed by running:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import inspect
|
|
||||||
from graphql.core.type import (
|
from graphql.core.type import (
|
||||||
GraphQLField,
|
GraphQLField,
|
||||||
GraphQLList,
|
GraphQLList,
|
||||||
|
@ -9,8 +8,8 @@ from graphql.core.type import (
|
||||||
GraphQLID,
|
GraphQLID,
|
||||||
GraphQLArgument,
|
GraphQLArgument,
|
||||||
)
|
)
|
||||||
from graphene.core.types import ObjectType, Interface
|
|
||||||
from graphene.utils import cached_property
|
from graphene.utils import cached_property
|
||||||
|
from graphene.core.utils import get_object_type
|
||||||
|
|
||||||
class Field(object):
|
class Field(object):
|
||||||
def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args):
|
def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args):
|
||||||
|
@ -45,16 +44,11 @@ class Field(object):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def type(self):
|
def type(self):
|
||||||
field_type = self.field_type
|
if isinstance(self.field_type, Field):
|
||||||
_is_class = inspect.isclass(field_type)
|
field_type = self.field_type.type
|
||||||
if _is_class and issubclass(field_type, ObjectType):
|
else:
|
||||||
field_type = field_type._meta.type
|
field_type = get_object_type(self.field_type, self.object_type)
|
||||||
elif isinstance(field_type, Field):
|
|
||||||
field_type = field_type.type
|
|
||||||
elif field_type == 'self':
|
|
||||||
field_type = self.object_type._meta.type
|
|
||||||
field_type = self.type_wrapper(field_type)
|
field_type = self.type_wrapper(field_type)
|
||||||
|
|
||||||
return field_type
|
return field_type
|
||||||
|
|
||||||
def type_wrapper(self, field_type):
|
def type_wrapper(self, field_type):
|
||||||
|
@ -105,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)
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
from graphene.utils import cached_property
|
from graphene.utils import cached_property
|
||||||
|
|
||||||
DEFAULT_NAMES = ('description', 'name', 'interface', 'type_name', 'interfaces', 'proxy')
|
DEFAULT_NAMES = ('app_label', 'description', 'name', 'interface',
|
||||||
|
'type_name', 'interfaces', 'proxy')
|
||||||
|
|
||||||
|
|
||||||
class Options(object):
|
class Options(object):
|
||||||
def __init__(self, meta=None):
|
def __init__(self, meta=None, app_label=None):
|
||||||
self.meta = meta
|
self.meta = meta
|
||||||
self.local_fields = []
|
self.local_fields = []
|
||||||
self.interface = False
|
self.interface = False
|
||||||
self.proxy = False
|
self.proxy = False
|
||||||
self.interfaces = []
|
self.interfaces = []
|
||||||
self.parents = []
|
self.parents = []
|
||||||
|
self.app_label = app_label
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
def contribute_to_class(self, cls, name):
|
||||||
cls._meta = self
|
cls._meta = self
|
||||||
|
@ -51,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):
|
||||||
|
@ -62,7 +64,7 @@ class Options(object):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def fields_map(self):
|
def fields_map(self):
|
||||||
return {f.field_name:f for f in self.fields}
|
return {f.field_name: f for f in self.fields}
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def type(self):
|
def type(self):
|
||||||
|
|
|
@ -8,8 +8,10 @@ from graphql.core.type import (
|
||||||
)
|
)
|
||||||
from graphql.core import graphql
|
from graphql.core import graphql
|
||||||
|
|
||||||
|
from graphene import signals
|
||||||
from graphene.core.options import Options
|
from graphene.core.options import Options
|
||||||
|
|
||||||
|
|
||||||
class ObjectTypeMeta(type):
|
class ObjectTypeMeta(type):
|
||||||
def __new__(cls, name, bases, attrs):
|
def __new__(cls, name, bases, attrs):
|
||||||
super_new = super(ObjectTypeMeta, cls).__new__
|
super_new = super(ObjectTypeMeta, cls).__new__
|
||||||
|
@ -20,7 +22,10 @@ class ObjectTypeMeta(type):
|
||||||
|
|
||||||
module = attrs.pop('__module__')
|
module = attrs.pop('__module__')
|
||||||
doc = attrs.pop('__doc__', None)
|
doc = attrs.pop('__doc__', None)
|
||||||
new_class = super_new(cls, name, bases, {'__module__': module, '__doc__': doc})
|
new_class = super_new(cls, name, bases, {
|
||||||
|
'__module__': module,
|
||||||
|
'__doc__': doc
|
||||||
|
})
|
||||||
attr_meta = attrs.pop('Meta', None)
|
attr_meta = attrs.pop('Meta', None)
|
||||||
if not attr_meta:
|
if not attr_meta:
|
||||||
meta = getattr(new_class, 'Meta', None)
|
meta = getattr(new_class, 'Meta', None)
|
||||||
|
@ -28,7 +33,12 @@ class ObjectTypeMeta(type):
|
||||||
meta = attr_meta
|
meta = attr_meta
|
||||||
base_meta = getattr(new_class, '_meta', None)
|
base_meta = getattr(new_class, '_meta', None)
|
||||||
|
|
||||||
new_class.add_to_class('_meta', Options(meta))
|
if '.' in module:
|
||||||
|
app_label, _ = module.rsplit('.', 1)
|
||||||
|
else:
|
||||||
|
app_label = module
|
||||||
|
|
||||||
|
new_class.add_to_class('_meta', Options(meta, app_label))
|
||||||
if base_meta and base_meta.proxy:
|
if base_meta and base_meta.proxy:
|
||||||
new_class._meta.interface = base_meta.interface
|
new_class._meta.interface = base_meta.interface
|
||||||
# Add all attributes to the class.
|
# Add all attributes to the class.
|
||||||
|
@ -51,7 +61,7 @@ class ObjectTypeMeta(type):
|
||||||
# moment).
|
# moment).
|
||||||
for field in parent_fields:
|
for field in parent_fields:
|
||||||
if field.field_name in field_names:
|
if field.field_name in field_names:
|
||||||
raise FieldError(
|
raise Exception(
|
||||||
'Local field %r in class %r clashes '
|
'Local field %r in class %r clashes '
|
||||||
'with field of similar name from '
|
'with field of similar name from '
|
||||||
'base class %r' % (field.field_name, name, base.__name__)
|
'base class %r' % (field.field_name, name, base.__name__)
|
||||||
|
@ -61,8 +71,12 @@ class ObjectTypeMeta(type):
|
||||||
new_class._meta.interfaces.append(base)
|
new_class._meta.interfaces.append(base)
|
||||||
# new_class._meta.parents.extend(base._meta.parents)
|
# new_class._meta.parents.extend(base._meta.parents)
|
||||||
|
|
||||||
|
new_class._prepare()
|
||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
|
def _prepare(cls):
|
||||||
|
signals.class_prepared.send(cls)
|
||||||
|
|
||||||
def add_to_class(cls, name, value):
|
def add_to_class(cls, name, value):
|
||||||
# We should call the contribute_to_class method only if it's bound
|
# We should call the contribute_to_class method only if it's bound
|
||||||
if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'):
|
if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'):
|
||||||
|
@ -73,15 +87,21 @@ class ObjectTypeMeta(type):
|
||||||
|
|
||||||
class ObjectType(six.with_metaclass(ObjectTypeMeta)):
|
class ObjectType(six.with_metaclass(ObjectTypeMeta)):
|
||||||
def __init__(self, instance=None):
|
def __init__(self, instance=None):
|
||||||
|
signals.pre_init.send(self.__class__, instance=instance)
|
||||||
self.instance = instance
|
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):
|
def get_field(self, field):
|
||||||
return getattr(self.instance, field, None)
|
return getattr(self.instance, field, None)
|
||||||
|
|
||||||
def resolve(self, field_name, args, info):
|
def resolve(self, field_name, args, info):
|
||||||
if field_name not in self._meta.fields_map.keys():
|
if field_name not in self._meta.fields_map.keys():
|
||||||
raise Exception('Field %s not found in model'%field_name)
|
raise Exception('Field %s not found in model' % field_name)
|
||||||
custom_resolve_fn = 'resolve_%s'%field_name
|
custom_resolve_fn = 'resolve_%s' % field_name
|
||||||
if hasattr(self, custom_resolve_fn):
|
if hasattr(self, custom_resolve_fn):
|
||||||
resolve_fn = getattr(self, custom_resolve_fn)
|
resolve_fn = getattr(self, custom_resolve_fn)
|
||||||
return resolve_fn(args, info)
|
return resolve_fn(args, info)
|
||||||
|
@ -104,13 +124,13 @@ class ObjectType(six.with_metaclass(ObjectTypeMeta)):
|
||||||
cls._meta.type_name,
|
cls._meta.type_name,
|
||||||
description=cls._meta.description,
|
description=cls._meta.description,
|
||||||
resolve_type=cls.resolve_type,
|
resolve_type=cls.resolve_type,
|
||||||
fields=lambda: {name:field.field for name, field in fields.items()}
|
fields=lambda: {name: field.field for name, field in fields.items()}
|
||||||
)
|
)
|
||||||
return GraphQLObjectType(
|
return GraphQLObjectType(
|
||||||
cls._meta.type_name,
|
cls._meta.type_name,
|
||||||
description=cls._meta.description,
|
description=cls._meta.description,
|
||||||
interfaces=[i._meta.type for i in cls._meta.interfaces],
|
interfaces=[i._meta.type for i in cls._meta.interfaces],
|
||||||
fields=lambda: {name:field.field for name, field in fields.items()}
|
fields=lambda: {name: field.field for name, field in fields.items()}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
54
graphene/core/utils.py
Normal file
54
graphene/core/utils.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from graphene.core.types import ObjectType
|
||||||
|
from graphene import signals
|
||||||
|
|
||||||
|
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):
|
||||||
|
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
|
||||||
|
|
||||||
|
if '.' in name:
|
||||||
|
app_label, object_type_name = name.rsplit('.', 1)
|
||||||
|
elif object_type:
|
||||||
|
app_label = object_type._meta.app_label
|
||||||
|
|
||||||
|
# Filter all registered object types which have the same name
|
||||||
|
ots = [ot for ot in registered_object_types if ot._meta.type_name == object_type_name]
|
||||||
|
# If the list have more than one object type with the name, filter by
|
||||||
|
# the app_label
|
||||||
|
if len(ots)>1 and app_label:
|
||||||
|
ots = [ot for ot in ots if ot._meta.app_label == app_label]
|
||||||
|
|
||||||
|
if len(ots)>1:
|
||||||
|
raise Exception('Multiple ObjectTypes returned with the name %s' % name)
|
||||||
|
if not ots:
|
||||||
|
raise Exception('No ObjectType found with name %s' % name)
|
||||||
|
|
||||||
|
return ots[0]
|
||||||
|
|
||||||
|
|
||||||
|
@signals.class_prepared.connect
|
||||||
|
def object_type_created(sender):
|
||||||
|
registered_object_types.append(sender)
|
|
@ -0,0 +1,85 @@
|
||||||
|
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)
|
5
graphene/signals.py
Normal file
5
graphene/signals.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from blinker import Signal
|
||||||
|
|
||||||
|
class_prepared = Signal()
|
||||||
|
pre_init = Signal()
|
||||||
|
post_init = Signal()
|
1
setup.py
1
setup.py
|
@ -48,6 +48,7 @@ setup(
|
||||||
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'six',
|
'six',
|
||||||
|
'blinker',
|
||||||
'graphqllib',
|
'graphqllib',
|
||||||
'graphql-relay'
|
'graphql-relay'
|
||||||
],
|
],
|
||||||
|
|
|
@ -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]
|
||||||
|
|
41
tests/relay/test_relay.py
Normal file
41
tests/relay/test_relay.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
from graphene import relay
|
||||||
|
|
||||||
|
|
||||||
|
class OtherNode(relay.Node):
|
||||||
|
name = graphene.StringField()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_node(cls, id):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_no_contributed_raises_error():
|
||||||
|
with raises(Exception) as excinfo:
|
||||||
|
class Part(relay.Node):
|
||||||
|
x = graphene.StringField()
|
||||||
|
|
||||||
|
assert 'get_node' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_should_have_connection():
|
||||||
|
assert OtherNode.connection
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_should_have_id_field():
|
||||||
|
assert 'id' in OtherNode._meta.fields_map
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_no_contributed_raises_error():
|
||||||
|
with raises(Exception) as excinfo:
|
||||||
|
class Ship(graphene.ObjectType):
|
||||||
|
name = graphene.StringField()
|
||||||
|
|
||||||
|
|
||||||
|
class Faction(relay.Node):
|
||||||
|
name = graphene.StringField()
|
||||||
|
ships = relay.ConnectionField(Ship)
|
||||||
|
|
||||||
|
assert 'same type_name' in str(excinfo.value)
|
|
@ -9,12 +9,14 @@ Episode = graphene.Enum('Episode', dict(
|
||||||
JEDI = 6
|
JEDI = 6
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
def wrap_character(character):
|
def wrap_character(character):
|
||||||
if isinstance(character, _Human):
|
if isinstance(character, _Human):
|
||||||
return Human(character)
|
return Human(character)
|
||||||
elif isinstance(character, _Droid):
|
elif isinstance(character, _Droid):
|
||||||
return Droid(character)
|
return Droid(character)
|
||||||
|
|
||||||
|
|
||||||
class Character(graphene.Interface):
|
class Character(graphene.Interface):
|
||||||
id = graphene.IDField()
|
id = graphene.IDField()
|
||||||
name = graphene.StringField()
|
name = graphene.StringField()
|
||||||
|
@ -24,6 +26,7 @@ class Character(graphene.Interface):
|
||||||
def resolve_friends(self, args, *_):
|
def resolve_friends(self, args, *_):
|
||||||
return [wrap_character(getCharacter(f)) for f in self.instance.friends]
|
return [wrap_character(getCharacter(f)) for f in self.instance.friends]
|
||||||
|
|
||||||
|
|
||||||
class Human(Character):
|
class Human(Character):
|
||||||
homePlanet = graphene.StringField()
|
homePlanet = graphene.StringField()
|
||||||
|
|
||||||
|
@ -50,8 +53,6 @@ class Query(graphene.ObjectType):
|
||||||
@resolve_only_args
|
@resolve_only_args
|
||||||
def resolve_human(self, id):
|
def resolve_human(self, id):
|
||||||
return wrap_character(getHuman(id))
|
return wrap_character(getHuman(id))
|
||||||
if human:
|
|
||||||
return Human(human)
|
|
||||||
|
|
||||||
@resolve_only_args
|
@resolve_only_args
|
||||||
def resolve_droid(self, id):
|
def resolve_droid(self, id):
|
||||||
|
|
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
|
53
tests/starwars_relay/schema.py
Normal file
53
tests/starwars_relay/schema.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import graphene
|
||||||
|
from graphene import resolve_only_args, relay
|
||||||
|
|
||||||
|
from .data import (
|
||||||
|
getFaction,
|
||||||
|
getShip,
|
||||||
|
getRebels,
|
||||||
|
getEmpire,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 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.')
|
||||||
|
|
||||||
|
@resolve_only_args
|
||||||
|
def resolve_ships(self, **kwargs):
|
||||||
|
return [Ship(getShip(ship)) for ship in self.instance.ships]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_node(cls, id):
|
||||||
|
faction = getFaction(id)
|
||||||
|
if faction:
|
||||||
|
return Faction(faction)
|
||||||
|
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
rebels = graphene.Field(Faction)
|
||||||
|
empire = graphene.Field(Faction)
|
||||||
|
node = relay.NodeField()
|
||||||
|
|
||||||
|
@resolve_only_args
|
||||||
|
def resolve_rebels(self):
|
||||||
|
return Faction(getRebels())
|
||||||
|
|
||||||
|
@resolve_only_args
|
||||||
|
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