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.
* **Easy to use:** It maps the models/fields to internal GraphQL objects without effort.
* **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
@ -16,26 +16,21 @@ pip install graphene
```
## Usage
## Examples
Example code of a GraphQL schema using Graphene:
### Schema definition
Here is one example for get you started:
```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):
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)
```
@ -44,48 +39,19 @@ Then Querying `graphene.Schema` is as simple as:
```python
query = '''
query HeroNameQuery {
hero {
name
}
query SayHello {
hello
ping(to:'peter')
}
'''
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

View File

@ -37,11 +37,20 @@ class DjangoObjectTypeMeta(ObjectTypeMeta):
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
class DjangoInterface(six.with_metaclass(DjangoObjectTypeMeta, BaseObjectType)):
class DjangoInterface(six.with_metaclass(DjangoObjectTypeMeta, InstanceObjectType)):
pass

View File

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

View File

@ -129,29 +129,48 @@ class ObjectTypeMeta(type):
class BaseObjectType(object):
def __new__(cls, instance=None, **kwargs):
def __new__(cls, *args, **kwargs):
if cls._meta.is_interface:
raise Exception("An interface cannot be initialized")
if instance is None:
if not kwargs:
return None
elif type(instance) is cls:
return instance
if not args and not kwargs:
return None
return super(BaseObjectType, cls).__new__(cls)
def __init__(self, instance=None, **kwargs):
signals.pre_init.send(self.__class__, instance=instance)
assert instance or kwargs
if not instance:
init_kwargs = dict({k: None for k in self._meta.fields_map.keys()}, **kwargs)
instance = self._meta.object(**init_kwargs)
self.instance = instance
signals.post_init.send(self.__class__, instance=self)
def __init__(self, *args, **kwargs):
signals.pre_init.send(self.__class__, args=args, kwargs=kwargs)
args_len = len(args)
fields = self._meta.fields
if args_len > len(fields):
# Daft, but matches old exception sans the err msg.
raise IndexError("Number of args exceeds number of fields")
fields_iter = iter(fields)
def __getattr__(self, name):
if self.instance:
return getattr(self.instance, name)
if not kwargs:
for val, field in zip(args, fields_iter):
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
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():
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 (
GraphQLNonNull,
GraphQLID
@ -10,11 +9,6 @@ from graphene import relay
schema = graphene.Schema()
class MyType(object):
name = 'my'
arg = None
class MyConnection(relay.Connection):
my_custom_field = graphene.StringField(resolve=lambda instance, *_: 'Custom')
@ -24,7 +18,7 @@ class MyNode(relay.Node):
@classmethod
def get_node(cls, id):
return MyNode(MyType())
return MyNode(name='mo')
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))
def resolve_all_my_nodes(self, args, info):
t = MyType()
custom_arg = args.get('customArg')
assert custom_arg == "1"
return [MyNode(t)]
return [MyNode(name='my')]
schema.query = Query
@ -61,7 +54,7 @@ def test_nodefield_query():
'''
expected = {
'myNode': {
'name': 'my'
'name': 'mo'
},
'allMyNodes': {
'edges': [{

View File

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

View File

@ -2,7 +2,7 @@ from graphql.core.type import GraphQLEnumValue
import graphene
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(
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):
id = graphene.IDField()
name = graphene.StringField()
friends = graphene.ListField('self')
appearsIn = graphene.ListField(Episode)
appears_in = graphene.ListField(Episode)
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):
homePlanet = graphene.StringField()
home_planet = graphene.StringField()
class Droid(Character):
primaryFunction = graphene.StringField()
primary_function = graphene.StringField()
class Query(graphene.ObjectType):
@ -52,15 +46,15 @@ class Query(graphene.ObjectType):
@resolve_only_args
def resolve_hero(self, episode=None):
return wrap_character(getHero(episode))
return getHero(episode)
@resolve_only_args
def resolve_human(self, id):
return wrap_character(getHuman(id))
return getHuman(id)
@resolve_only_args
def resolve_droid(self, id):
return wrap_character(getDroid(id))
return getDroid(id)
Schema = graphene.Schema(query=Query)

View File

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

View File

@ -27,7 +27,7 @@ class Ship(DjangoNode):
@schema.register
class CharacterModel(DjangoObjectType):
class Character(DjangoObjectType):
class Meta:
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(
id='1',
name='X-Wing',
)
def setup():
global data
ywing = Ship(
id='2',
name='Y-Wing',
)
from .schema import Ship, Faction
xwing = Ship(
id='1',
name='X-Wing',
)
awing = Ship(
id='3',
name='A-Wing',
)
ywing = Ship(
id='2',
name='Y-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',
)
awing = Ship(
id='3',
name='A-Wing',
)
homeOne = Ship(
id='5',
name='Home One',
)
# 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',
)
tieFighter = Ship(
id='6',
name='TIE Fighter',
)
homeOne = Ship(
id='5',
name='Home One',
)
tieInterceptor = Ship(
id='7',
name='TIE Interceptor',
)
tieFighter = Ship(
id='6',
name='TIE Fighter',
)
executor = Ship(
id='8',
name='Executor',
)
tieInterceptor = Ship(
id='7',
name='TIE Interceptor',
)
rebels = Faction(
id='1',
name='Alliance to Restore the Republic',
ships=['1', '2', '3', '4', '5']
)
executor = Ship(
id='8',
name='Executor',
)
empire = Faction(
id='2',
name='Galactic Empire',
ships=['6', '7', '8']
)
rebels = Faction(
id='1',
name='Alliance to Restore the Republic',
ships=['1', '2', '3', '4', '5']
)
data = {
'Faction': {
'1': rebels,
'2': empire
},
'Ship': {
'1': xwing,
'2': ywing,
'3': awing,
'4': falcon,
'5': homeOne,
'6': tieFighter,
'7': tieInterceptor,
'8': executor
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):
@ -95,8 +97,8 @@ def getFaction(_id):
def getRebels():
return rebels
return getFaction('1')
def getEmpire():
return empire
return getFaction('2')

View File

@ -12,29 +12,28 @@ schema = graphene.Schema(name='Starwars Relay Schema')
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):
return Ship(getShip(id))
return getShip(id)
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]
def resolve_ships(self, **args):
# Transform the instance ship_ids into real instances
return [getShip(ship_id) for ship_id in self.ships]
@classmethod
def get_node(cls, id):
return Faction(getFaction(id))
return getFaction(id)
class Query(graphene.ObjectType):
@ -44,11 +43,11 @@ class Query(graphene.ObjectType):
@resolve_only_args
def resolve_rebels(self):
return Faction(getRebels())
return getRebels()
@resolve_only_args
def resolve_empire(self):
return Faction(getEmpire())
return getEmpire()
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 .data import setup
setup()
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 .data import setup
setup()
def test_correctly_fetches_id_name_rebels():