Improved types as containers

This commit is contained in:
Syrus Akbary 2015-10-26 23:54:51 -07:00
parent 2958cc18af
commit 129999d41a
15 changed files with 234 additions and 324 deletions

View File

@ -1,10 +1,10 @@
# ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master) # ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![PyPI version](https://badge.fury.io/py/graphene.svg)](https://badge.fury.io/py/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master)
Graphene is a Python library for building GraphQL schemas/types fast and easily. Graphene is a Python library for building GraphQL schemas/types fast and easily.
* **Easy to use:** It maps the models/fields to internal GraphQL objects without effort. * **Easy to use:** It maps the models/fields to internal GraphQL objects without effort.
* **Relay:** Graphene has builtin support for Relay * **Relay:** Graphene has builtin support for Relay
* **Django:** Automatic [Django models](#djangorelay-schema) conversion. *See an [example Django](http://github.com/graphql-python/swapi-graphene) implementation* * **Django:** Automatic *Django model* mapping to Graphene Types. *See an [example Django](http://github.com/graphql-python/swapi-graphene) implementation*
## Installation ## Installation
@ -16,26 +16,21 @@ pip install graphene
``` ```
## Usage ## Examples
Example code of a GraphQL schema using Graphene: Here is one example for get you started:
### Schema definition
```python ```python
class Character(graphene.Interface):
id = graphene.IDField()
name = graphene.StringField()
friends = graphene.ListField('self')
def resolve_friends(self, args, *_):
return [Human(f) for f in self.instance.friends]
class Human(Character):
homePlanet = graphene.StringField()
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
human = graphene.Field(Human) hello = graphene.StringField(description='A typical hello world')
ping = graphene.StringField(description='Ping someone',
to=graphene.Argument(graphene.String))
def resolve_hello(self, args, info):
return 'World'
def resolve_ping(self, args, info):
return 'Pinging {}'.format(args.get('to'))
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
``` ```
@ -44,48 +39,19 @@ Then Querying `graphene.Schema` is as simple as:
```python ```python
query = ''' query = '''
query HeroNameQuery { query SayHello {
hero { hello
name ping(to:'peter')
}
} }
''' '''
result = schema.execute(query) result = schema.execute(query)
``` ```
### Relay Schema If you want to learn even more, you can also check the following examples:
Graphene also supports Relay, check the [Starwars Relay example](tests/starwars_relay)! * Relay Schema: [Starwars Relay example](tests/starwars_relay)
* Django: [Starwars Django example](tests/starwars_django)
```python
class Ship(relay.Node):
name = graphene.StringField()
@classmethod
def get_node(cls, id):
return Ship(your_ship_instance)
class Query(graphene.ObjectType):
ships = relay.ConnectionField(Ship)
node = relay.NodeField()
```
### Django+Relay Schema
If you want to use graphene with your Django Models check the [Starwars Django example](tests/starwars_django)!
```python
class Ship(DjangoNode):
class Meta:
model = YourDjangoModelHere
# only_fields = ('id', 'name') # Only map this fields from the model
# exclude_fields ('field_to_exclude', ) # Exclude mapping this fields from the model
class Query(graphene.ObjectType):
node = relay.NodeField()
```
## Contributing ## Contributing

View File

@ -37,11 +37,20 @@ class DjangoObjectTypeMeta(ObjectTypeMeta):
cls.add_to_class(field.name, converted_field) cls.add_to_class(field.name, converted_field)
class DjangoObjectType(six.with_metaclass(DjangoObjectTypeMeta, BaseObjectType)): class InstanceObjectType(BaseObjectType):
def __init__(self, instance=None):
self.instance = instance
super(InstanceObjectType, self).__init__()
def __getattr__(self, attr):
return getattr(self.instance, attr)
class DjangoObjectType(six.with_metaclass(DjangoObjectTypeMeta, InstanceObjectType)):
pass pass
class DjangoInterface(six.with_metaclass(DjangoObjectTypeMeta, BaseObjectType)): class DjangoInterface(six.with_metaclass(DjangoObjectTypeMeta, InstanceObjectType)):
pass pass

View File

@ -28,7 +28,7 @@ class Field(object):
creation_counter = 0 creation_counter = 0
required = False required = False
def __init__(self, field_type, name=None, resolve=None, required=False, args=None, description='', **extra_args): def __init__(self, field_type, name=None, resolve=None, required=False, args=None, description='', default=None, **extra_args):
self.field_type = field_type self.field_type = field_type
self.resolve_fn = resolve self.resolve_fn = resolve
self.required = self.required or required self.required = self.required or required
@ -38,9 +38,13 @@ class Field(object):
self.name = name self.name = name
self.description = description or self.__doc__ self.description = description or self.__doc__
self.object_type = None self.object_type = None
self.default = default
self.creation_counter = Field.creation_counter self.creation_counter = Field.creation_counter
Field.creation_counter += 1 Field.creation_counter += 1
def get_default(self):
return self.default
def contribute_to_class(self, cls, name, add=True): def contribute_to_class(self, cls, name, add=True):
if not self.name: if not self.name:
self.name = to_camel_case(name) self.name = to_camel_case(name)
@ -57,7 +61,7 @@ class Field(object):
if resolve_fn: if resolve_fn:
return resolve_fn(instance, args, info) return resolve_fn(instance, args, info)
else: else:
return getattr(instance, self.field_name, None) return getattr(instance, self.field_name, self.get_default())
def get_resolve_fn(self, schema): def get_resolve_fn(self, schema):
object_type = self.get_object_type(schema) object_type = self.get_object_type(schema)

View File

@ -129,29 +129,48 @@ class ObjectTypeMeta(type):
class BaseObjectType(object): class BaseObjectType(object):
def __new__(cls, instance=None, **kwargs): def __new__(cls, *args, **kwargs):
if cls._meta.is_interface: if cls._meta.is_interface:
raise Exception("An interface cannot be initialized") raise Exception("An interface cannot be initialized")
if instance is None: if not args and not kwargs:
if not kwargs: return None
return None
elif type(instance) is cls:
return instance
return super(BaseObjectType, cls).__new__(cls) return super(BaseObjectType, cls).__new__(cls)
def __init__(self, instance=None, **kwargs): def __init__(self, *args, **kwargs):
signals.pre_init.send(self.__class__, instance=instance) signals.pre_init.send(self.__class__, args=args, kwargs=kwargs)
assert instance or kwargs args_len = len(args)
if not instance: fields = self._meta.fields
init_kwargs = dict({k: None for k in self._meta.fields_map.keys()}, **kwargs) if args_len > len(fields):
instance = self._meta.object(**init_kwargs) # Daft, but matches old exception sans the err msg.
self.instance = instance raise IndexError("Number of args exceeds number of fields")
signals.post_init.send(self.__class__, instance=self) fields_iter = iter(fields)
def __getattr__(self, name): if not kwargs:
if self.instance: for val, field in zip(args, fields_iter):
return getattr(self.instance, name) setattr(self, field.field_name, val)
else:
for val, field in zip(args, fields_iter):
setattr(self, field.field_name, val)
kwargs.pop(field.field_name, None)
for field in fields_iter:
try:
val = kwargs.pop(field.field_name)
setattr(self, field.field_name, val)
except KeyError:
pass
if kwargs:
for prop in list(kwargs):
try:
if isinstance(getattr(self.__class__, prop), property):
setattr(self, prop, kwargs.pop(prop))
except AttributeError:
pass
if kwargs:
raise TypeError("'%s' is an invalid keyword argument for this function" % list(kwargs)[0])
signals.post_init.send(self.__class__, instance=self)
@classmethod @classmethod
def fields_as_arguments(cls, schema): def fields_as_arguments(cls, schema):

View File

@ -32,21 +32,3 @@ def test_node_should_have_same_connection_always():
def test_node_should_have_id_field(): def test_node_should_have_id_field():
assert 'id' in OtherNode._meta.fields_map 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 Meta:
# schema = schema
# class Faction(relay.Node):
# name = graphene.StringField()
# ships = relay.ConnectionField(Ship)
# @classmethod
# def get_node(cls):
# pass
# class Meta:
# schema = schema
# assert 'same type_name' in str(excinfo.value)

View File

@ -1,4 +1,3 @@
from pytest import raises
from graphql.core.type import ( from graphql.core.type import (
GraphQLNonNull, GraphQLNonNull,
GraphQLID GraphQLID
@ -10,11 +9,6 @@ from graphene import relay
schema = graphene.Schema() schema = graphene.Schema()
class MyType(object):
name = 'my'
arg = None
class MyConnection(relay.Connection): class MyConnection(relay.Connection):
my_custom_field = graphene.StringField(resolve=lambda instance, *_: 'Custom') my_custom_field = graphene.StringField(resolve=lambda instance, *_: 'Custom')
@ -24,7 +18,7 @@ class MyNode(relay.Node):
@classmethod @classmethod
def get_node(cls, id): def get_node(cls, id):
return MyNode(MyType()) return MyNode(name='mo')
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
@ -32,10 +26,9 @@ class Query(graphene.ObjectType):
all_my_nodes = relay.ConnectionField(MyNode, connection_type=MyConnection, customArg=graphene.Argument(graphene.String)) all_my_nodes = relay.ConnectionField(MyNode, connection_type=MyConnection, customArg=graphene.Argument(graphene.String))
def resolve_all_my_nodes(self, args, info): def resolve_all_my_nodes(self, args, info):
t = MyType()
custom_arg = args.get('customArg') custom_arg = args.get('customArg')
assert custom_arg == "1" assert custom_arg == "1"
return [MyNode(t)] return [MyNode(name='my')]
schema.query = Query schema.query = Query
@ -61,7 +54,7 @@ def test_nodefield_query():
''' '''
expected = { expected = {
'myNode': { 'myNode': {
'name': 'my' 'name': 'mo'
}, },
'allMyNodes': { 'allMyNodes': {
'edges': [{ 'edges': [{

View File

@ -1,77 +1,78 @@
from collections import namedtuple humanData = {}
droidData = {}
Human = namedtuple('Human', 'id name friends appearsIn homePlanet')
luke = Human( def setup():
id='1000', from .schema import Human, Droid
name='Luke Skywalker', global humanData, droidData
friends=['1002', '1003', '2000', '2001'], luke = Human(
appearsIn=[4, 5, 6], id='1000',
homePlanet='Tatooine', name='Luke Skywalker',
) friends=['1002', '1003', '2000', '2001'],
appears_in=[4, 5, 6],
home_planet='Tatooine',
)
vader = Human( vader = Human(
id='1001', id='1001',
name='Darth Vader', name='Darth Vader',
friends=['1004'], friends=['1004'],
appearsIn=[4, 5, 6], appears_in=[4, 5, 6],
homePlanet='Tatooine', home_planet='Tatooine',
) )
han = Human( han = Human(
id='1002', id='1002',
name='Han Solo', name='Han Solo',
friends=['1000', '1003', '2001'], friends=['1000', '1003', '2001'],
appearsIn=[4, 5, 6], appears_in=[4, 5, 6],
homePlanet=None, home_planet=None,
) )
leia = Human( leia = Human(
id='1003', id='1003',
name='Leia Organa', name='Leia Organa',
friends=['1000', '1002', '2000', '2001'], friends=['1000', '1002', '2000', '2001'],
appearsIn=[4, 5, 6], appears_in=[4, 5, 6],
homePlanet='Alderaan', home_planet='Alderaan',
) )
tarkin = Human( tarkin = Human(
id='1004', id='1004',
name='Wilhuff Tarkin', name='Wilhuff Tarkin',
friends=['1001'], friends=['1001'],
appearsIn=[4], appears_in=[4],
homePlanet=None, home_planet=None,
) )
humanData = { humanData = {
'1000': luke, '1000': luke,
'1001': vader, '1001': vader,
'1002': han, '1002': han,
'1003': leia, '1003': leia,
'1004': tarkin, '1004': tarkin,
} }
Droid = namedtuple('Droid', 'id name friends appearsIn primaryFunction') threepio = Droid(
id='2000',
name='C-3PO',
friends=['1000', '1002', '1003', '2001'],
appears_in=[4, 5, 6],
primary_function='Protocol',
)
threepio = Droid( artoo = Droid(
id='2000', id='2001',
name='C-3PO', name='R2-D2',
friends=['1000', '1002', '1003', '2001'], friends=['1000', '1002', '1003'],
appearsIn=[4, 5, 6], appears_in=[4, 5, 6],
primaryFunction='Protocol', primary_function='Astromech',
) )
artoo = Droid( droidData = {
id='2001', '2000': threepio,
name='R2-D2', '2001': artoo,
friends=['1000', '1002', '1003'], }
appearsIn=[4, 5, 6],
primaryFunction='Astromech',
)
droidData = {
'2000': threepio,
'2001': artoo,
}
def getCharacter(id): def getCharacter(id):
@ -84,8 +85,8 @@ def getFriends(character):
def getHero(episode): def getHero(episode):
if episode == 5: if episode == 5:
return luke return humanData['1000']
return artoo return droidData['2001']
def getHuman(id): def getHuman(id):

View File

@ -2,7 +2,7 @@ from graphql.core.type import GraphQLEnumValue
import graphene import graphene
from graphene import resolve_only_args from graphene import resolve_only_args
from .data import getHero, getHuman, getCharacter, getDroid, Human as _Human, Droid as _Droid from .data import getHero, getHuman, getCharacter, getDroid
Episode = graphene.Enum('Episode', dict( Episode = graphene.Enum('Episode', dict(
NEWHOPE=GraphQLEnumValue(4), NEWHOPE=GraphQLEnumValue(4),
@ -11,29 +11,23 @@ Episode = graphene.Enum('Episode', dict(
)) ))
def wrap_character(character):
if isinstance(character, _Human):
return Human(character)
elif isinstance(character, _Droid):
return Droid(character)
class Character(graphene.Interface): class Character(graphene.Interface):
id = graphene.IDField() id = graphene.IDField()
name = graphene.StringField() name = graphene.StringField()
friends = graphene.ListField('self') friends = graphene.ListField('self')
appearsIn = graphene.ListField(Episode) appears_in = graphene.ListField(Episode)
def resolve_friends(self, args, *_): def resolve_friends(self, args, *_):
return [wrap_character(getCharacter(f)) for f in self.instance.friends] # The character friends is a list of strings
return [getCharacter(f) for f in self.friends]
class Human(Character): class Human(Character):
homePlanet = graphene.StringField() home_planet = graphene.StringField()
class Droid(Character): class Droid(Character):
primaryFunction = graphene.StringField() primary_function = graphene.StringField()
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
@ -52,15 +46,15 @@ class Query(graphene.ObjectType):
@resolve_only_args @resolve_only_args
def resolve_hero(self, episode=None): def resolve_hero(self, episode=None):
return wrap_character(getHero(episode)) return getHero(episode)
@resolve_only_args @resolve_only_args
def resolve_human(self, id): def resolve_human(self, id):
return wrap_character(getHuman(id)) return getHuman(id)
@resolve_only_args @resolve_only_args
def resolve_droid(self, id): def resolve_droid(self, id):
return wrap_character(getDroid(id)) return getDroid(id)
Schema = graphene.Schema(query=Query) Schema = graphene.Schema(query=Query)

View File

@ -1,6 +1,8 @@
from .schema import Schema, Query from .schema import Schema, Query
from graphql.core import graphql from graphql.core import graphql
from .data import setup
setup()
def test_hero_name_query(): def test_hero_name_query():
query = ''' query = '''

View File

@ -27,7 +27,7 @@ class Ship(DjangoNode):
@schema.register @schema.register
class CharacterModel(DjangoObjectType): class Character(DjangoObjectType):
class Meta: class Meta:
model = CharacterModel model = CharacterModel

View File

@ -1,78 +1,80 @@
from collections import namedtuple data = {}
Ship = namedtuple('Ship', ['id', 'name'])
Faction = namedtuple('Faction', ['id', 'name', 'ships'])
xwing = Ship( def setup():
id='1', global data
name='X-Wing',
)
ywing = Ship( from .schema import Ship, Faction
id='2', xwing = Ship(
name='Y-Wing', id='1',
) name='X-Wing',
)
awing = Ship( ywing = Ship(
id='3', id='2',
name='A-Wing', name='Y-Wing',
) )
# Yeah, technically it's Corellian. But it flew in the service of the rebels, awing = Ship(
# so for the purposes of this demo it's a rebel ship. id='3',
falcon = Ship( name='A-Wing',
id='4', )
name='Millenium Falcon',
)
homeOne = Ship( # Yeah, technically it's Corellian. But it flew in the service of the rebels,
id='5', # so for the purposes of this demo it's a rebel ship.
name='Home One', falcon = Ship(
) id='4',
name='Millenium Falcon',
)
tieFighter = Ship( homeOne = Ship(
id='6', id='5',
name='TIE Fighter', name='Home One',
) )
tieInterceptor = Ship( tieFighter = Ship(
id='7', id='6',
name='TIE Interceptor', name='TIE Fighter',
) )
executor = Ship( tieInterceptor = Ship(
id='8', id='7',
name='Executor', name='TIE Interceptor',
) )
rebels = Faction( executor = Ship(
id='1', id='8',
name='Alliance to Restore the Republic', name='Executor',
ships=['1', '2', '3', '4', '5'] )
)
empire = Faction( rebels = Faction(
id='2', id='1',
name='Galactic Empire', name='Alliance to Restore the Republic',
ships=['6', '7', '8'] ships=['1', '2', '3', '4', '5']
) )
data = { empire = Faction(
'Faction': { id='2',
'1': rebels, name='Galactic Empire',
'2': empire ships=['6', '7', '8']
}, )
'Ship': {
'1': xwing, data = {
'2': ywing, 'Faction': {
'3': awing, '1': rebels,
'4': falcon, '2': empire
'5': homeOne, },
'6': tieFighter, 'Ship': {
'7': tieInterceptor, '1': xwing,
'8': executor '2': ywing,
'3': awing,
'4': falcon,
'5': homeOne,
'6': tieFighter,
'7': tieInterceptor,
'8': executor
}
} }
}
def createShip(shipName, factionId): def createShip(shipName, factionId):
@ -95,8 +97,8 @@ def getFaction(_id):
def getRebels(): def getRebels():
return rebels return getFaction('1')
def getEmpire(): def getEmpire():
return empire return getFaction('2')

View File

@ -12,29 +12,28 @@ schema = graphene.Schema(name='Starwars Relay Schema')
class Ship(relay.Node): class Ship(relay.Node):
'''A ship in the Star Wars saga''' '''A ship in the Star Wars saga'''
name = graphene.StringField(description='The name of the ship.') name = graphene.StringField(description='The name of the ship.')
@classmethod @classmethod
def get_node(cls, id): def get_node(cls, id):
return Ship(getShip(id)) return getShip(id)
class Faction(relay.Node): class Faction(relay.Node):
'''A faction in the Star Wars saga''' '''A faction in the Star Wars saga'''
name = graphene.StringField(description='The name of the faction.') name = graphene.StringField(description='The name of the faction.')
ships = relay.ConnectionField( ships = relay.ConnectionField(
Ship, description='The ships used by the faction.') Ship, description='The ships used by the faction.')
@resolve_only_args @resolve_only_args
def resolve_ships(self, **kwargs): def resolve_ships(self, **args):
return [Ship(getShip(ship)) for ship in self.instance.ships] # Transform the instance ship_ids into real instances
return [getShip(ship_id) for ship_id in self.ships]
@classmethod @classmethod
def get_node(cls, id): def get_node(cls, id):
return Faction(getFaction(id)) return getFaction(id)
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
@ -44,11 +43,11 @@ class Query(graphene.ObjectType):
@resolve_only_args @resolve_only_args
def resolve_rebels(self): def resolve_rebels(self):
return Faction(getRebels()) return getRebels()
@resolve_only_args @resolve_only_args
def resolve_empire(self): def resolve_empire(self):
return Faction(getEmpire()) return getEmpire()
schema.query = Query schema.query = Query

View File

@ -1,61 +0,0 @@
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

@ -1,7 +1,7 @@
from pytest import raises
from graphql.core import graphql
from .schema import schema from .schema import schema
from .data import setup
setup()
def test_correct_fetch_first_ship_rebels(): def test_correct_fetch_first_ship_rebels():

View File

@ -1,7 +1,7 @@
from pytest import raises
from graphql.core import graphql
from .schema import schema from .schema import schema
from .data import setup
setup()
def test_correctly_fetches_id_name_rebels(): def test_correctly_fetches_id_name_rebels():