Merge pull request #16 from graphql-python/1.0

Path to 0.3
This commit is contained in:
Syrus Akbary 2015-10-30 00:46:44 -07:00
commit 3e8e65f033
64 changed files with 1281 additions and 734 deletions

2
.gitignore vendored
View File

@ -59,3 +59,5 @@ docs/_build/
# PyBuilder # PyBuilder
target/ target/
/tests/django.sqlite

View File

@ -6,9 +6,10 @@ python:
- 3.4 - 3.4
- 3.5 - 3.5
- pypy - pypy
cache: pip
install: install:
- pip install pytest pytest-cov coveralls flake8 six blinker pytest-django - pip install --cache-dir $HOME/.cache/pip pytest pytest-cov coveralls flake8 six blinker pytest-django
- pip install -e .[django] - pip install --cache-dir $HOME/.cache/pip -e .[django]
- python setup.py develop - python setup.py develop
script: script:
- py.test --cov=graphene - py.test --cov=graphene

View File

@ -1,10 +1,13 @@
# ![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:** Graphene helps you use GraphQL in Python 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*
*But, what is supported in this Python version?* **Everything**: Interfaces, ObjectTypes, Mutations and Relay (Nodes, Connections and Mutations).
## Installation ## Installation
@ -13,29 +16,26 @@ For instaling graphene, just run this command in your shell
```bash ```bash
pip install graphene pip install graphene
# Or in case of need Django model support
pip install graphene[django]
``` ```
## 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 +44,20 @@ 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](examples/):
Graphene also supports Relay, check the [Starwars Relay example](tests/starwars_relay)! * **Basic Schema**: [Starwars example](examples/starwars)
* **Relay Schema**: [Starwars Relay example](examples/starwars_relay)
* **Django model mapping**: [Starwars Django example](examples/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

@ -1,9 +1,16 @@
Graphene |Build Status| |Coverage Status| |Graphene Logo| `Graphene <http://graphene-python.org>`__ |Build Status| |PyPI version| |Coverage Status|
========================================= =========================================================================================================
Graphene is a Python library for creating GraphQL schemas/types easly. Graphene is a Python library for building GraphQL schemas/types fast and
It maps the models/fields to internal GraphQL objects without effort. easily. \* **Easy to use:** Graphene helps you use GraphQL in Python
Including automatic `Django models`_ conversion. without effort. \* **Relay:** Graphene has builtin support for Relay \*
**Django:** Automatic *Django model* mapping to Graphene Types. *See an
`example Django <http://github.com/graphql-python/swapi-graphene>`__
implementation*
*But, what is supported in this Python version?* **Everything**:
Interfaces, ObjectTypes, Mutations and Relay (Nodes, Connections and
Mutations).
Installation Installation
------------ ------------
@ -13,89 +20,49 @@ For instaling graphene, just run this command in your shell
.. code:: bash .. code:: bash
pip install graphene pip install graphene
# Or in case of need Django model support
pip install graphene[django]
Usage Examples
----- --------
Example code of a GraphQL schema using Graphene: Here is one example for get you started:
Schema definition
~~~~~~~~~~~~~~~~~
.. code:: python .. code:: 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)
Querying Then Querying ``graphene.Schema`` is as simple as:
~~~~~~~~
Querying ``graphene.Schema`` is as simple as:
.. code:: python .. code:: 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 <examples/>`__:
Graphene also supports Relay, check the `Starwars Relay example`_! - **Basic Schema**: `Starwars example <examples/starwars>`__
- **Relay Schema**: `Starwars Relay
.. code:: python example <examples/starwars_relay>`__
- **Django model mapping**: `Starwars Django
class Ship(relay.Node): example <examples/starwars_django>`__
'''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))
class Query(graphene.ObjectType):
ships = relay.ConnectionField(Ship, description='The ships used by the faction.')
node = relay.NodeField()
@resolve_only_args
def resolve_ships(self):
return [Ship(s) for s in getShips()]
Django+Relay Schema
~~~~~~~~~~~~~~~~~~~
If you want to use graphene with your Django Models check the `Starwars
Django example`_!
.. code:: python
class Ship(DjangoNode):
class Meta:
model = YourDjangoModelHere
# only_fields = ('id', 'name') # Only map this fields from the model
# excluxe_fields ('field_to_excluxe', ) # Exclude mapping this fields from the model
class Query(graphene.ObjectType):
node = relay.NodeField()
Contributing Contributing
------------ ------------
@ -112,11 +79,10 @@ After developing, the full test suite can be evaluated by running:
python setup.py test # Use --pytest-args="-v -s" for verbose mode python setup.py test # Use --pytest-args="-v -s" for verbose mode
.. _Django models: #djangorelay-schema .. |Graphene Logo| image:: http://graphene-python.org/favicon.png
.. _Starwars Relay example: tests/starwars_relay
.. _Starwars Django example: tests/starwars_django
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master .. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master
:target: https://travis-ci.org/graphql-python/graphene :target: https://travis-ci.org/graphql-python/graphene
.. |PyPI version| image:: https://badge.fury.io/py/graphene.svg
:target: https://badge.fury.io/py/graphene
.. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github .. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github
:target: https://coveralls.io/github/graphql-python/graphene?branch=master :target: https://coveralls.io/github/graphql-python/graphene?branch=master

3
bin/convert_documentation Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
pandoc README.md --from markdown --to rst -s -o README.rst

97
examples/starwars/data.py Normal file
View File

@ -0,0 +1,97 @@
humanData = {}
droidData = {}
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'],
appears_in=[4, 5, 6],
home_planet='Tatooine',
)
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'],
appears_in=[4, 5, 6],
home_planet='Alderaan',
)
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,
}
threepio = Droid(
id='2000',
name='C-3PO',
friends=['1000', '1002', '1003', '2001'],
appears_in=[4, 5, 6],
primary_function='Protocol',
)
artoo = Droid(
id='2001',
name='R2-D2',
friends=['1000', '1002', '1003'],
appears_in=[4, 5, 6],
primary_function='Astromech',
)
droidData = {
'2000': threepio,
'2001': artoo,
}
def getCharacter(id):
return humanData.get(id) or droidData.get(id)
def getFriends(character):
return map(getCharacter, character.friends)
def getHero(episode):
if episode == 5:
return humanData['1000']
return droidData['2001']
def getHuman(id):
return humanData.get(id)
def getDroid(id):
return droidData.get(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):
@ -47,20 +41,17 @@ class Query(graphene.ObjectType):
id=graphene.Argument(graphene.String) id=graphene.Argument(graphene.String)
) )
class Meta:
type_name = 'core_Query'
@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

View File

@ -85,31 +85,30 @@ def initialize():
executor.save() executor.save()
def createShip(shipName, factionId): def create_ship(ship_name, faction_id):
nextShip = len(data['Ship'].keys())+1 new_ship = Ship(
newShip = Ship( name=ship_name,
id=str(nextShip), faction_id=faction_id
name=shipName
) )
newShip.save() new_ship.save()
return newShip return new_ship
def getShip(_id): def get_ship(_id):
return Ship.objects.get(id=_id) return Ship.objects.get(id=_id)
def getShips(): def get_ships():
return Ship.objects.all() return Ship.objects.all()
def getFaction(_id): def get_faction(_id):
return Faction.objects.get(id=_id) return Faction.objects.get(id=_id)
def getRebels(): def get_rebels():
return getFaction(1) return get_faction(1)
def getEmpire(): def get_empire():
return getFaction(2) return get_faction(2)

View File

@ -7,11 +7,12 @@ from graphene.contrib.django import (
from .models import ( from .models import (
Ship as ShipModel, Faction as FactionModel, Character as CharacterModel) Ship as ShipModel, Faction as FactionModel, Character as CharacterModel)
from .data import ( from .data import (
getFaction, get_faction,
getShip, get_ship,
getShips, get_ships,
getRebels, get_rebels,
getEmpire, get_empire,
create_ship
) )
schema = graphene.Schema(name='Starwars Django Relay Schema') schema = graphene.Schema(name='Starwars Django Relay Schema')
@ -23,11 +24,11 @@ class Ship(DjangoNode):
@classmethod @classmethod
def get_node(cls, id): def get_node(cls, id):
return Ship(getShip(id)) return Ship(get_ship(id))
@schema.register @schema.register
class CharacterModel(DjangoObjectType): class Character(DjangoObjectType):
class Meta: class Meta:
model = CharacterModel model = CharacterModel
@ -38,7 +39,24 @@ class Faction(DjangoNode):
@classmethod @classmethod
def get_node(cls, id): def get_node(cls, id):
return Faction(getFaction(id)) return Faction(get_faction(id))
class IntroduceShip(relay.ClientIDMutation):
class Input:
ship_name = graphene.StringField(required=True)
faction_id = graphene.StringField(required=True)
ship = graphene.Field(Ship)
faction = graphene.Field(Faction)
@classmethod
def mutate_and_get_payload(cls, input, info):
ship_name = input.get('ship_name')
faction_id = input.get('faction_id')
ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id)
return IntroduceShip(ship=ship, faction=faction)
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
@ -49,15 +67,20 @@ class Query(graphene.ObjectType):
@resolve_only_args @resolve_only_args
def resolve_ships(self): def resolve_ships(self):
return [Ship(s) for s in getShips()] return [Ship(s) for s in get_ships()]
@resolve_only_args @resolve_only_args
def resolve_rebels(self): def resolve_rebels(self):
return Faction(getRebels()) return Faction(get_rebels())
@resolve_only_args @resolve_only_args
def resolve_empire(self): def resolve_empire(self):
return Faction(getEmpire()) return Faction(get_empire())
class Mutation(graphene.ObjectType):
introduce_ship = graphene.Field(IntroduceShip)
schema.query = Query schema.query = Query
schema.mutation = Mutation

View File

@ -1,9 +1,9 @@
import pytest import pytest
from graphql.core import graphql from graphql.core import graphql
from .models import * from ..models import *
from .schema import schema from ..schema import schema
from .data import initialize from ..data import initialize
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db

View File

@ -0,0 +1,78 @@
import pytest
from ..schema import schema
from ..data import initialize
pytestmark = pytest.mark.django_db
def test_mutations():
initialize()
query = '''
mutation MyMutation {
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
ship {
id
name
}
faction {
name
ships {
edges {
node {
id
name
}
}
}
}
}
}
'''
expected = {
'introduceShip': {
'ship': {
'id': 'U2hpcDo5',
'name': 'Peter'
},
'faction': {
'name': 'Alliance to Restore the Republic',
'ships': {
'edges': [{
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}, {
'node': {
'id': 'U2hpcDoy',
'name': 'Y-Wing'
}
}, {
'node': {
'id': 'U2hpcDoz',
'name': 'A-Wing'
}
}, {
'node': {
'id': 'U2hpcDo0',
'name': 'Millenium Falcon'
}
}, {
'node': {
'id': 'U2hpcDo1',
'name': 'Home One'
}
}, {
'node': {
'id': 'U2hpcDo5',
'name': 'Peter'
}
}]
},
}
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected

View File

@ -1,9 +1,9 @@
import pytest import pytest
from pytest import raises from pytest import raises
from graphql.core import graphql from graphql.core import graphql
from .data import initialize from ..data import initialize
from .schema import schema from ..schema import schema
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db

View File

View File

@ -0,0 +1,105 @@
data = {}
def setup():
global data
from .schema import Ship, Faction
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 create_ship(ship_name, faction_id):
from .schema import Ship
next_ship = len(data['Ship'].keys()) + 1
new_ship = Ship(
id=str(next_ship),
name=ship_name
)
data['Ship'][new_ship.id] = new_ship
data['Faction'][faction_id].ships.append(new_ship.id)
return new_ship
def get_ship(_id):
return data['Ship'][_id]
def get_faction(_id):
return data['Faction'][_id]
def get_rebels():
return get_faction('1')
def get_empire():
return get_faction('2')

View File

@ -0,0 +1,76 @@
import graphene
from graphene import resolve_only_args, relay
from .data import (
get_faction,
get_ship,
get_rebels,
get_empire,
create_ship,
)
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 get_ship(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, **args):
# Transform the instance ship_ids into real instances
return [get_ship(ship_id) for ship_id in self.ships]
@classmethod
def get_node(cls, id):
return get_faction(id)
class IntroduceShip(relay.ClientIDMutation):
class Input:
ship_name = graphene.StringField(required=True)
faction_id = graphene.StringField(required=True)
ship = graphene.Field(Ship)
faction = graphene.Field(Faction)
@classmethod
def mutate_and_get_payload(cls, input, info):
ship_name = input.get('ship_name')
faction_id = input.get('faction_id')
ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id)
return IntroduceShip(ship=ship, 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 get_rebels()
@resolve_only_args
def resolve_empire(self):
return get_empire()
class Mutation(graphene.ObjectType):
introduce_ship = graphene.Field(IntroduceShip)
schema.query = Query
schema.mutation = Mutation

View File

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

View File

@ -0,0 +1,75 @@
from ..schema import schema
from ..data import setup
setup()
def test_mutations():
query = '''
mutation MyMutation {
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
ship {
id
name
}
faction {
name
ships {
edges {
node {
id
name
}
}
}
}
}
}
'''
expected = {
'introduceShip': {
'ship': {
'id': 'U2hpcDo5',
'name': 'Peter'
},
'faction': {
'name': 'Alliance to Restore the Republic',
'ships': {
'edges': [{
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}, {
'node': {
'id': 'U2hpcDoy',
'name': 'Y-Wing'
}
}, {
'node': {
'id': 'U2hpcDoz',
'name': 'A-Wing'
}
}, {
'node': {
'id': 'U2hpcDo0',
'name': 'Millenium Falcon'
}
}, {
'node': {
'id': 'U2hpcDo1',
'name': 'Home One'
}
}, {
'node': {
'id': 'U2hpcDo5',
'name': 'Peter'
}
}]
},
}
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected

View File

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

View File

@ -14,7 +14,8 @@ from graphene.core.schema import (
from graphene.core.types import ( from graphene.core.types import (
ObjectType, ObjectType,
Interface Interface,
Mutation,
) )
from graphene.core.fields import ( from graphene.core.fields import (
@ -31,3 +32,8 @@ from graphene.core.fields import (
from graphene.decorators import ( from graphene.decorators import (
resolve_only_args resolve_only_args
) )
__all__ = ['Enum', 'Argument', 'String', 'Int', 'ID', 'signals', 'Schema',
'ObjectType', 'Interface', 'Mutation', 'Field', 'StringField',
'IntField', 'BooleanField', 'IDField', 'ListField', 'NonNullField',
'FloatField', 'resolve_only_args']

View File

@ -4,9 +4,7 @@ from graphene.core.fields import (
from graphene import relay from graphene import relay
from graphene.core.fields import Field, LazyField from graphene.core.fields import Field, LazyField
from graphene.utils import cached_property, memoize, LazyMap
from graphene.relay.types import BaseNode
from graphene.relay.utils import is_node from graphene.relay.utils import is_node
from graphene.contrib.django.utils import get_type_for_model, lazy_map from graphene.contrib.django.utils import get_type_for_model, lazy_map
@ -25,7 +23,6 @@ class LazyListField(ListField):
class ConnectionOrListField(LazyField): class ConnectionOrListField(LazyField):
@memoize
def get_field(self, schema): def get_field(self, schema):
model_field = self.field_type model_field = self.field_type
field_object_type = model_field.get_object_type(schema) field_object_type = model_field.get_object_type(schema)
@ -52,7 +49,6 @@ class DjangoModelField(Field):
)) ))
return _type(resolved) return _type(resolved)
@memoize
def internal_type(self, schema): def internal_type(self, schema):
_type = self.get_object_type(schema) _type = self.get_object_type(schema)
if not _type and self.object_type._meta.only_fields: if not _type and self.object_type._meta.only_fields:
@ -66,7 +62,7 @@ class DjangoModelField(Field):
self.object_type self.object_type
) )
) )
return _type and _type.internal_type(schema) or Field.SKIP return schema.T(_type) or Field.SKIP
def get_object_type(self, schema): def get_object_type(self, schema):
return get_type_for_model(schema, self.model) return get_type_for_model(schema, self.model)

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

@ -2,10 +2,9 @@ from django.db import models
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.db.models.manager import Manager from django.db.models.manager import Manager
from graphene.utils import memoize, LazyMap from graphene.utils import LazyMap
# @memoize
def get_type_for_model(schema, model): def get_type_for_model(schema, model):
schema = schema schema = schema
types = schema.types.values() types = schema.types.values()

View File

@ -1,5 +1,10 @@
import inspect import inspect
import six import six
try:
from enum import Enum
except ImportError:
class Enum(object):
pass
from functools import total_ordering, wraps from functools import total_ordering, wraps
from graphql.core.type import ( from graphql.core.type import (
GraphQLField, GraphQLField,
@ -11,9 +16,10 @@ from graphql.core.type import (
GraphQLID, GraphQLID,
GraphQLArgument, GraphQLArgument,
GraphQLFloat, GraphQLFloat,
GraphQLInputObjectField,
) )
from graphene.utils import memoize, to_camel_case from graphene.utils import to_camel_case, ProxySnakeDict, enum_to_graphql_enum
from graphene.core.types import BaseObjectType from graphene.core.types import BaseObjectType, InputObjectType
from graphene.core.scalars import GraphQLSkipField from graphene.core.scalars import GraphQLSkipField
@ -27,7 +33,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
@ -37,9 +43,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)
@ -51,20 +61,18 @@ class Field(object):
cls._meta.add_field(self) cls._meta.add_field(self)
def resolve(self, instance, args, info): def resolve(self, instance, args, info):
resolve_fn = self.get_resolve_fn() schema = info and getattr(info.schema, 'graphene_schema', None)
resolve_fn = self.get_resolve_fn(schema)
if resolve_fn: if resolve_fn:
return resolve_fn(instance, args, info) return resolve_fn(instance, ProxySnakeDict(args), info)
else: else:
if isinstance(instance, BaseObjectType): return getattr(instance, self.field_name, self.get_default())
return instance.get_field(self.field_name)
if hasattr(instance, self.field_name):
return getattr(instance, self.field_name)
elif hasattr(instance, self.name):
return getattr(instance, self.name)
@memoize def get_resolve_fn(self, schema):
def get_resolve_fn(self): object_type = self.get_object_type(schema)
if self.resolve_fn: if object_type and object_type._meta.is_mutation:
return object_type.mutate
elif self.resolve_fn:
return self.resolve_fn return self.resolve_fn
else: else:
custom_resolve_fn_name = 'resolve_%s' % self.field_name custom_resolve_fn_name = 'resolve_%s' % self.field_name
@ -96,24 +104,25 @@ class Field(object):
field_type = GraphQLNonNull(field_type) field_type = GraphQLNonNull(field_type)
return field_type return field_type
@memoize
def internal_type(self, schema): def internal_type(self, schema):
field_type = self.field_type field_type = self.field_type
_is_class = inspect.isclass(field_type)
if isinstance(field_type, Field): if isinstance(field_type, Field):
field_type = self.field_type.internal_type(schema) field_type = self.field_type.internal_type(schema)
elif _is_class and issubclass(field_type, Enum):
field_type = enum_to_graphql_enum(field_type)
else: else:
object_type = self.get_object_type(schema) object_type = self.get_object_type(schema)
if object_type: if object_type:
field_type = object_type.internal_type(schema) field_type = schema.T(object_type)
field_type = self.type_wrapper(field_type) field_type = self.type_wrapper(field_type)
return field_type return field_type
@memoize
def internal_field(self, schema): def internal_field(self, schema):
if not self.object_type: if not self.object_type:
raise Exception( raise Exception(
'Field could not be constructed in a non graphene.Type or graphene.Interface') 'Field could not be constructed in a non graphene.ObjectType or graphene.Interface')
extra_args = self.extra_args.copy() extra_args = self.extra_args.copy()
for arg_name, arg_value in self.extra_args.items(): for arg_name, arg_value in self.extra_args.items():
@ -128,27 +137,44 @@ class Field(object):
','.join(extra_args.keys()) ','.join(extra_args.keys())
)) ))
args = self.args
object_type = self.get_object_type(schema)
if object_type and object_type._meta.is_mutation:
assert not self.args, 'Arguments provided for mutations are defined in Input class in Mutation'
args = object_type.get_input_type().fields_as_arguments(schema)
internal_type = self.internal_type(schema) internal_type = self.internal_type(schema)
if not internal_type: if not internal_type:
raise Exception("Internal type for field %s is None" % self) raise Exception("Internal type for field %s is None" % self)
resolve_fn = self.get_resolve_fn() description = self.description
resolve_fn = self.get_resolve_fn(schema)
if resolve_fn: if resolve_fn:
description = resolve_fn.__doc__ or description
@wraps(resolve_fn) @wraps(resolve_fn)
def resolver(*args): def resolver(*args):
return self.resolve(*args) return self.resolve(*args)
else: else:
resolver = self.resolve resolver = self.resolve
if issubclass(self.object_type, InputObjectType):
return GraphQLInputObjectField(
internal_type,
description=description,
)
return GraphQLField( return GraphQLField(
internal_type, internal_type,
description=self.description, description=description,
args=self.args, args=args,
resolver=resolver, resolver=resolver,
) )
def __str__(self): def __str__(self):
""" Return "object_type.name". """ """ Return "object_type.name". """
return '%s.%s' % (self.object_type, self.field_name) return '%s.%s' % (self.object_type.__name__, self.field_name)
def __repr__(self): def __repr__(self):
""" """
@ -189,7 +215,6 @@ class Field(object):
class LazyField(Field): class LazyField(Field):
@memoize
def inner_field(self, schema): def inner_field(self, schema):
return self.get_field(schema) return self.get_field(schema)

View File

@ -1,7 +1,7 @@
from graphene.utils import cached_property from graphene.utils import cached_property
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
DEFAULT_NAMES = ('description', 'name', 'interface', DEFAULT_NAMES = ('description', 'name', 'is_interface', 'is_mutation',
'type_name', 'interfaces', 'proxy') 'type_name', 'interfaces', 'proxy')
@ -10,7 +10,8 @@ class Options(object):
def __init__(self, meta=None): def __init__(self, meta=None):
self.meta = meta self.meta = meta
self.local_fields = [] self.local_fields = []
self.interface = False self.is_interface = False
self.is_mutation = False
self.proxy = False self.proxy = False
self.interfaces = [] self.interfaces = []
self.parents = [] self.parents = []
@ -54,15 +55,8 @@ class Options(object):
else: else:
self.proxy = False self.proxy = False
if self.interfaces != [] and self.interface:
raise Exception("A interface cannot inherit from interfaces")
del self.meta del self.meta
@cached_property
def object(self):
return namedtuple(self.type_name, self.fields_map.keys())
def add_field(self, field): def add_field(self, field):
self.local_fields.append(field) self.local_fields.append(field)

View File

@ -25,6 +25,7 @@ class Schema(object):
def __init__(self, query=None, mutation=None, name='Schema', executor=None): def __init__(self, query=None, mutation=None, name='Schema', executor=None):
self._internal_types = {} self._internal_types = {}
self._types = {}
self.mutation = mutation self.mutation = mutation
self.query = query self.query = query
self.name = name self.name = name
@ -34,6 +35,13 @@ class Schema(object):
def __repr__(self): def __repr__(self):
return '<Schema: %s (%s)>' % (str(self.name), hash(self)) return '<Schema: %s (%s)>' % (str(self.name), hash(self))
def T(self, object_type):
if not object_type:
return
if object_type not in self._types:
self._types[object_type] = object_type.internal_type(self)
return self._types[object_type]
@property @property
def query(self): def query(self):
return self._query return self._query
@ -41,7 +49,14 @@ class Schema(object):
@query.setter @query.setter
def query(self, query): def query(self, query):
self._query = query self._query = query
self._query_type = query and query.internal_type(self)
@property
def mutation(self):
return self._mutation
@mutation.setter
def mutation(self, mutation):
self._mutation = mutation
@property @property
def executor(self): def executor(self):
@ -53,11 +68,11 @@ class Schema(object):
def executor(self, value): def executor(self, value):
self._executor = value self._executor = value
@cached_property @property
def schema(self): def schema(self):
if not self._query_type: if not self._query:
raise Exception('You have to define a base query type') raise Exception('You have to define a base query type')
return GraphQLSchema(self, query=self._query_type, mutation=self.mutation) return GraphQLSchema(self, query=self.T(self._query), mutation=self.T(self._mutation))
def associate_internal_type(self, internal_type, object_type): def associate_internal_type(self, internal_type, object_type):
self._internal_types[internal_type.name] = object_type self._internal_types[internal_type.name] = object_type
@ -67,6 +82,7 @@ class Schema(object):
return object_type return object_type
def get_type(self, type_name): def get_type(self, type_name):
self.schema._build_type_map()
if type_name not in self._internal_types: if type_name not in self._internal_types:
raise Exception('Type %s not found in %r' % (type_name, self)) raise Exception('Type %s not found in %r' % (type_name, self))
return self._internal_types[type_name] return self._internal_types[type_name]

View File

@ -5,7 +5,9 @@ from collections import OrderedDict
from graphql.core.type import ( from graphql.core.type import (
GraphQLObjectType, GraphQLObjectType,
GraphQLInterfaceType GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLArgument
) )
from graphene import signals from graphene import signals
@ -21,7 +23,7 @@ class ObjectTypeMeta(type):
return Interface in parents return Interface in parents
def is_mutation(cls, parents): def is_mutation(cls, parents):
return Mutation in parents return issubclass(cls, Mutation)
def __new__(cls, name, bases, attrs): def __new__(cls, name, bases, attrs):
super_new = super(ObjectTypeMeta, cls).__new__ super_new = super(ObjectTypeMeta, cls).__new__
@ -47,14 +49,28 @@ class ObjectTypeMeta(type):
new_class.add_to_class('_meta', new_class.options_cls(meta)) new_class.add_to_class('_meta', new_class.options_cls(meta))
new_class._meta.interface = new_class.is_interface(parents) new_class._meta.is_interface = new_class.is_interface(parents)
new_class._meta.mutation = new_class.is_mutation(parents) new_class._meta.is_mutation = new_class.is_mutation(parents)
assert not (new_class._meta.interface and new_class._meta.mutation) assert not (new_class._meta.is_interface and new_class._meta.is_mutation)
input_class = None
if new_class._meta.is_mutation:
input_class = attrs.pop('Input', None)
# Add all attributes to the class. # Add all attributes to the class.
for obj_name, obj in attrs.items(): for obj_name, obj in attrs.items():
new_class.add_to_class(obj_name, obj) new_class.add_to_class(obj_name, obj)
if new_class._meta.is_mutation:
assert hasattr(new_class, 'mutate'), "All mutations must implement mutate method"
if input_class:
items = dict(input_class.__dict__)
items.pop('__dict__', None)
input_type = type('{}Input'.format(new_class._meta.type_name), (ObjectType, ), items)
new_class.add_to_class('input_type', input_type)
new_class.add_extra_fields() new_class.add_extra_fields()
new_fields = new_class._meta.local_fields new_fields = new_class._meta.local_fields
@ -88,7 +104,7 @@ class ObjectTypeMeta(type):
new_class.add_to_class(field.field_name, new_field) new_class.add_to_class(field.field_name, new_field)
new_class._meta.parents.append(base) new_class._meta.parents.append(base)
if base._meta.interface: if base._meta.is_interface:
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)
@ -113,50 +129,67 @@ class ObjectTypeMeta(type):
class BaseObjectType(object): class BaseObjectType(object):
def __new__(cls, instance=None, **kwargs): def __new__(cls, *args, **kwargs):
if cls._meta.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 kwargs:
return None
elif type(instance) is cls:
instance = instance.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")
fields_iter = iter(fields)
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) signals.post_init.send(self.__class__, instance=self)
def __getattr__(self, name): @classmethod
if self.instance: def fields_as_arguments(cls, schema):
return getattr(self.instance, name) return OrderedDict([(f.field_name, GraphQLArgument(f.internal_type(schema)))
for f in cls._meta.fields])
def get_field(self, field):
return getattr(self.instance, field, None)
@classmethod @classmethod
def resolve_objecttype(cls, schema, instance, *_): def resolve_objecttype(cls, schema, instance, *_):
return instance return instance.__class__
@classmethod @classmethod
def resolve_type(cls, schema, instance, *_): def resolve_type(cls, schema, instance, *_):
objecttype = cls.resolve_objecttype(schema, instance, *_) objecttype = cls.resolve_objecttype(schema, instance, *_)
return objecttype.internal_type(schema) return schema.T(objecttype)
@classmethod @classmethod
@memoize
@register_internal_type @register_internal_type
def internal_type(cls, schema): def internal_type(cls, schema):
fields_list = cls._meta.fields
fields = lambda: OrderedDict([(f.name, f.internal_field(schema)) fields = lambda: OrderedDict([(f.name, f.internal_field(schema))
for f in fields_list]) for f in cls._meta.fields])
if cls._meta.interface: if cls._meta.is_interface:
return GraphQLInterfaceType( return GraphQLInterfaceType(
cls._meta.type_name, cls._meta.type_name,
description=cls._meta.description, description=cls._meta.description,
@ -167,19 +200,34 @@ class BaseObjectType(object):
return GraphQLObjectType( return GraphQLObjectType(
cls._meta.type_name, cls._meta.type_name,
description=cls._meta.description, description=cls._meta.description,
interfaces=[i.internal_type(schema) for i in cls._meta.interfaces], interfaces=[schema.T(i) for i in cls._meta.interfaces],
fields=fields, fields=fields,
is_type_of=getattr(cls, 'is_type_of', None) is_type_of=getattr(cls, 'is_type_of', None)
) )
class Interface(six.with_metaclass(ObjectTypeMeta, BaseObjectType)):
pass
class ObjectType(six.with_metaclass(ObjectTypeMeta, BaseObjectType)): class ObjectType(six.with_metaclass(ObjectTypeMeta, BaseObjectType)):
pass pass
class Mutation(six.with_metaclass(ObjectTypeMeta, BaseObjectType)): class Mutation(six.with_metaclass(ObjectTypeMeta, BaseObjectType)):
pass @classmethod
def get_input_type(cls):
return getattr(cls, 'input_type', None)
class Interface(six.with_metaclass(ObjectTypeMeta, BaseObjectType)): class InputObjectType(ObjectType):
pass @classmethod
@register_internal_type
def internal_type(cls, schema):
fields = lambda: OrderedDict([(f.name, f.internal_field(schema))
for f in cls._meta.fields])
return GraphQLInputObjectType(
cls._meta.type_name,
description=cls._meta.description,
fields=fields,
)

View File

@ -8,7 +8,8 @@ from graphene.relay.types import (
Node, Node,
PageInfo, PageInfo,
Edge, Edge,
Connection Connection,
ClientIDMutation
) )
from graphene.relay.utils import is_node from graphene.relay.utils import is_node

View File

@ -16,7 +16,6 @@ from graphql.core.type import (
) )
from graphene.core.fields import Field, IDField from graphene.core.fields import Field, IDField
from graphene.utils import memoize
class ConnectionField(Field): class ConnectionField(Field):
@ -51,24 +50,21 @@ class ConnectionField(Field):
connection.set_connection_data(resolved) connection.set_connection_data(resolved)
return connection return connection
@memoize
def get_connection_type(self, node): def get_connection_type(self, node):
connection_type = self.connection_type or node.get_connection_type() connection_type = self.connection_type or node.get_connection_type()
edge_type = self.get_edge_type(node) edge_type = self.get_edge_type(node)
return connection_type.for_node(node, edge_type=edge_type) return connection_type.for_node(node, edge_type=edge_type)
@memoize
def get_edge_type(self, node): def get_edge_type(self, node):
return self.edge_type or node.get_edge_type() return self.edge_type or node.get_edge_type()
@memoize
def internal_type(self, schema): def internal_type(self, schema):
from graphene.relay.utils import is_node from graphene.relay.utils import is_node
node = self.get_object_type(schema) node = self.get_object_type(schema)
assert is_node(node), 'Only nodes have connections.' assert is_node(node), 'Only nodes have connections.'
schema.register(node) schema.register(node)
connection_type = self.get_connection_type(node)
return self.get_connection_type(node).internal_type(schema) return schema.T(connection_type)
class NodeField(Field): class NodeField(Field):

View File

@ -2,7 +2,7 @@ from graphql_relay.node.node import (
to_global_id to_global_id
) )
from graphene.core.types import Interface, ObjectType from graphene.core.types import Interface, ObjectType, Mutation, InputObjectType
from graphene.core.fields import BooleanField, StringField, ListField, Field from graphene.core.fields import BooleanField, StringField, ListField, Field
from graphene.relay.fields import GlobalIDField from graphene.relay.fields import GlobalIDField
from graphene.utils import memoize from graphene.utils import memoize
@ -84,3 +84,32 @@ class BaseNode(object):
class Node(BaseNode, Interface): class Node(BaseNode, Interface):
'''An object with an ID''' '''An object with an ID'''
id = GlobalIDField() id = GlobalIDField()
class MutationInputType(InputObjectType):
client_mutation_id = StringField(required=True)
class ClientIDMutation(Mutation):
client_mutation_id = StringField(required=True)
@classmethod
def _prepare_class(cls):
input_type = getattr(cls, 'input_type', None)
if input_type:
assert hasattr(cls, 'mutate_and_get_payload'), 'You have to implement mutate_and_get_payload'
new_input_inner_type = type('{}InnerInput'.format(cls._meta.type_name), (MutationInputType, input_type, ), {})
items = {
'input': Field(new_input_inner_type)
}
assert issubclass(new_input_inner_type, InputObjectType)
input_type = type('{}Input'.format(cls._meta.type_name), (ObjectType, ), items)
setattr(cls, 'input_type', input_type)
@classmethod
def mutate(cls, instance, args, info):
input = args.get('input')
payload = cls.mutate_and_get_payload(input, info)
client_mutation_id = input.get('client_mutation_id')
setattr(payload, 'client_mutation_id', client_mutation_id)
return payload

View File

@ -1,86 +0,0 @@
from functools import wraps
class cached_property(object):
"""
A property that is only computed once per instance and then replaces itself
with an ordinary attribute. Deleting the attribute resets the property.
Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
""" # noqa
def __init__(self, func):
self.__doc__ = getattr(func, '__doc__')
self.func = func
def __get__(self, obj, cls):
if obj is None:
return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value
def memoize(fun):
"""A simple memoize decorator for functions supporting positional args."""
@wraps(fun)
def wrapper(*args, **kwargs):
key = (args, frozenset(sorted(kwargs.items())))
try:
return cache[key]
except KeyError:
ret = cache[key] = fun(*args, **kwargs)
return ret
cache = {}
return wrapper
# From this response in Stackoverflow
# http://stackoverflow.com/a/19053800/1072990
def to_camel_case(snake_str):
components = snake_str.split('_')
# We capitalize the first letter of each component except the first one
# with the 'title' method and join them together.
return components[0] + "".join(x.title() for x in components[1:])
class LazyMap(object):
def __init__(self, origin, _map, state=None):
self._origin = origin
self._origin_iter = origin.__iter__()
self._state = state or []
self._finished = False
self._map = _map
def __iter__(self):
return self if not self._finished else iter(self._state)
def iter(self):
return self.__iter__()
def __len__(self):
return self._origin.__len__()
def __next__(self):
try:
n = next(self._origin_iter)
n = self._map(n)
except StopIteration as e:
self._finished = True
raise e
else:
self._state.append(n)
return n
def next(self):
return self.__next__()
def __getitem__(self, key):
item = self._origin.__getitem__(key)
if isinstance(key, slice):
return LazyMap(item, self._map)
return self._map(item)
def __getattr__(self, name):
return getattr(self._origin, name)
def __repr__(self):
return "<LazyMap %s>" % repr(self._origin)

View File

@ -0,0 +1,8 @@
from .str_converters import to_camel_case, to_snake_case
from .proxy_snake_dict import ProxySnakeDict
from .caching import cached_property, memoize
from .lazymap import LazyMap
from .misc import enum_to_graphql_enum
__all__ = ['to_camel_case', 'to_snake_case', 'ProxySnakeDict',
'cached_property', 'memoize', 'LazyMap', 'enum_to_graphql_enum']

35
graphene/utils/caching.py Normal file
View File

@ -0,0 +1,35 @@
from functools import wraps
class CachedPropery(object):
"""
A property that is only computed once per instance and then replaces itself
with an ordinary attribute. Deleting the attribute resets the property.
Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
""" # noqa
def __init__(self, func):
self.__doc__ = getattr(func, '__doc__')
self.func = func
def __get__(self, obj, cls):
if obj is None:
return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value
cached_property = CachedPropery
def memoize(fun):
"""A simple memoize decorator for functions supporting positional args."""
@wraps(fun)
def wrapper(*args, **kwargs):
key = (args, frozenset(sorted(kwargs.items())))
try:
return cache[key]
except KeyError:
ret = cache[key] = fun(*args, **kwargs)
return ret
cache = {}
return wrapper

42
graphene/utils/lazymap.py Normal file
View File

@ -0,0 +1,42 @@
class LazyMap(object):
def __init__(self, origin, _map, state=None):
self._origin = origin
self._origin_iter = origin.__iter__()
self._state = state or []
self._finished = False
self._map = _map
def __iter__(self):
return self if not self._finished else iter(self._state)
def iter(self):
return self.__iter__()
def __len__(self):
return self._origin.__len__()
def __next__(self):
try:
n = next(self._origin_iter)
n = self._map(n)
except StopIteration as e:
self._finished = True
raise e
else:
self._state.append(n)
return n
def next(self):
return self.__next__()
def __getitem__(self, key):
item = self._origin.__getitem__(key)
if isinstance(key, slice):
return LazyMap(item, self._map)
return self._map(item)
def __getattr__(self, name):
return getattr(self._origin, name)
def __repr__(self):
return "<LazyMap %s>" % repr(self._origin)

10
graphene/utils/misc.py Normal file
View File

@ -0,0 +1,10 @@
from collections import OrderedDict
from graphql.core.type import GraphQLEnumType, GraphQLEnumValue
def enum_to_graphql_enum(enumeration):
return GraphQLEnumType(
name=enumeration.__name__,
values=OrderedDict([(it.name, GraphQLEnumValue(it.value)) for it in enumeration]),
description=enumeration.__doc__
)

View File

@ -0,0 +1,70 @@
import collections
from .str_converters import to_camel_case, to_snake_case
class ProxySnakeDict(collections.MutableMapping):
__slots__ = ('data')
def __init__(self, data):
self.data = data
def __contains__(self, key):
return key in self.data or to_camel_case(key) in self.data
def get(self, key, default=None):
try:
return self.__getitem__(key)
except KeyError:
return default
def __iter__(self):
return self.iterkeys()
def __len__(self):
return len(self.data)
def __delitem__(self, item):
raise TypeError('ProxySnakeDict does not support item deletion')
def __setitem__(self, item, value):
raise TypeError('ProxySnakeDict does not support item assignment')
def __getitem__(self, key):
if key in self.data:
item = self.data[key]
else:
camel_key = to_camel_case(key)
if camel_key in self.data:
item = self.data[camel_key]
else:
raise KeyError(key, camel_key)
if isinstance(item, dict):
return ProxySnakeDict(item)
return item
def keys(self):
return list(self.iterkeys())
def items(self):
return list(self.iteritems())
def iterkeys(self):
for k in self.data.keys():
yield to_snake_case(k)
return
def iteritems(self):
for k in self.iterkeys():
yield k, self[k]
def to_data_dict(self):
return self.data.__class__(self.iteritems())
def __eq__(self, other):
return self.to_data_dict() == other.to_data_dict()
def __repr__(self):
data_repr = self.to_data_dict().__repr__()
return '<ProxySnakeDict {}>'.format(data_repr)

View File

@ -0,0 +1,17 @@
import re
# From this response in Stackoverflow
# http://stackoverflow.com/a/19053800/1072990
def to_camel_case(snake_str):
components = snake_str.split('_')
# We capitalize the first letter of each component except the first one
# with the 'title' method and join them together.
return components[0] + "".join(x.title() for x in components[1:])
# From this response in Stackoverflow
# http://stackoverflow.com/a/1176023/1072990
def to_snake_case(name):
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()

View File

@ -24,7 +24,7 @@ class PyTest(TestCommand):
setup( setup(
name='graphene', name='graphene',
version='0.1.6.1', version='0.3.0',
description='Graphene: Python DSL for GraphQL', description='Graphene: Python DSL for GraphQL',
long_description=open('README.rst').read(), long_description=open('README.rst').read(),

View File

@ -46,12 +46,12 @@ schema = Schema()
def test_django_interface(): def test_django_interface():
assert DjangoNode._meta.interface is True assert DjangoNode._meta.is_interface is True
def test_pseudo_interface(): def test_pseudo_interface():
object_type = Character.internal_type(schema) object_type = schema.T(Character)
assert Character._meta.interface is True assert Character._meta.is_interface is True
assert isinstance(object_type, GraphQLInterfaceType) assert isinstance(object_type, GraphQLInterfaceType)
assert Character._meta.model == Reporter assert Character._meta.model == Reporter
assert_equal_lists( assert_equal_lists(
@ -81,9 +81,9 @@ def test_interface_resolve_type():
def test_object_type(): def test_object_type():
object_type = Human.internal_type(schema) object_type = schema.T(Human)
fields_map = Human._meta.fields_map fields_map = Human._meta.fields_map
assert Human._meta.interface is False assert Human._meta.is_interface is False
assert isinstance(object_type, GraphQLObjectType) assert isinstance(object_type, GraphQLObjectType)
assert_equal_lists( assert_equal_lists(
object_type.get_fields().keys(), object_type.get_fields().keys(),
@ -95,9 +95,9 @@ def test_object_type():
# 'reporter': fields_map['reporter'].internal_field(schema), # 'reporter': fields_map['reporter'].internal_field(schema),
# 'pubDate': fields_map['pub_date'].internal_field(schema), # 'pubDate': fields_map['pub_date'].internal_field(schema),
# } # }
assert DjangoNode.internal_type(schema) in object_type.get_interfaces() assert schema.T(DjangoNode) in object_type.get_interfaces()
def test_node_notinterface(): def test_node_notinterface():
assert Human._meta.interface is False assert Human._meta.is_interface is False
assert DjangoNode in Human._meta.interfaces assert DjangoNode in Human._meta.interfaces

View File

@ -24,19 +24,14 @@ from graphene.core.types import ObjectType
class ObjectType(object): class ObjectType(object):
_meta = Options() _meta = Options()
def resolve(self, *args, **kwargs): def resolve_customdoc(self, *args, **kwargs):
'''Resolver documentation'''
return None return None
def can_resolve(self, *args):
return True
def __str__(self): def __str__(self):
return "ObjectType" return "ObjectType"
ot = ObjectType() ot = ObjectType
ObjectType._meta.contribute_to_class(ObjectType, '_meta')
schema = Schema() schema = Schema()
@ -89,7 +84,7 @@ def test_field_resolve():
f = StringField(required=True, resolve=lambda *args: 'RESOLVED') f = StringField(required=True, resolve=lambda *args: 'RESOLVED')
f.contribute_to_class(ot, 'field_name') f.contribute_to_class(ot, 'field_name')
field_type = f.internal_field(schema) field_type = f.internal_field(schema)
assert 'RESOLVED' == field_type.resolver(ot, 2, 3) assert 'RESOLVED' == field_type.resolver(ot, None, None)
def test_field_resolve_type_custom(): def test_field_resolve_type_custom():
@ -172,3 +167,10 @@ def test_field_repr_contributed():
f = StringField() f = StringField()
f.contribute_to_class(ot, 'field_name') f.contribute_to_class(ot, 'field_name')
assert repr(f) == "<graphene.core.fields.StringField: field_name>" assert repr(f) == "<graphene.core.fields.StringField: field_name>"
def test_field_resolve_objecttype_cos():
f = StringField()
f.contribute_to_class(ot, 'customdoc')
field = f.internal_field(schema)
assert field.description == 'Resolver documentation'

View File

@ -0,0 +1,66 @@
import graphene
from py.test import raises
from graphene.core.schema import Schema
my_id = 0
class Query(graphene.ObjectType):
base = graphene.StringField()
class ChangeNumber(graphene.Mutation):
'''Result mutation'''
class Input:
to = graphene.IntField()
result = graphene.StringField()
@classmethod
def mutate(cls, instance, args, info):
global my_id
my_id = args.get('to', my_id + 1)
return ChangeNumber(result=my_id)
class MyResultMutation(graphene.ObjectType):
change_number = graphene.Field(ChangeNumber)
schema = Schema(query=Query, mutation=MyResultMutation)
def test_mutation_input():
assert ChangeNumber.input_type
assert ChangeNumber.input_type._meta.type_name == 'ChangeNumberInput'
assert list(ChangeNumber.input_type._meta.fields_map.keys()) == ['to']
def test_execute_mutations():
query = '''
mutation M{
first: changeNumber {
result
},
second: changeNumber {
result
}
third: changeNumber(to: 5) {
result
}
}
'''
expected = {
'first': {
'result': '1',
},
'second': {
'result': '2',
},
'third': {
'result': '5',
}
}
result = schema.execute(query, root=object())
assert not result.errors
assert result.data == expected

View File

@ -10,7 +10,7 @@ from graphene.core.options import Options
class Meta: class Meta:
interface = True is_interface = True
type_name = 'Character' type_name = 'Character'

View File

@ -46,11 +46,11 @@ class Human(Character):
schema = Schema() schema = Schema()
Human_type = Human.internal_type(schema) Human_type = schema.T(Human)
def test_type(): def test_type():
assert Human._meta.fields_map['name'].resolve(Human(object()), 1, 2) == 'Peter' assert Human._meta.fields_map['name'].resolve(Human(object()), None, None) == 'Peter'
def test_query(): def test_query():

View File

@ -125,9 +125,23 @@ def test_schema_register():
class MyType(ObjectType): class MyType(ObjectType):
type = StringField(resolve=lambda *_: 'Dog') type = StringField(resolve=lambda *_: 'Dog')
schema.query = MyType
assert schema.get_type('MyType') == MyType assert schema.get_type('MyType') == MyType
def test_schema_register():
schema = Schema(name='My own schema')
@schema.register
class MyType(ObjectType):
type = StringField(resolve=lambda *_: 'Dog')
with raises(Exception) as excinfo:
schema.get_type('MyType')
assert 'base query type' in str(excinfo.value)
def test_schema_introspect(): def test_schema_introspect():
schema = Schema(name='My own schema') schema = Schema(name='My own schema')
@ -138,4 +152,3 @@ def test_schema_introspect():
introspection = schema.introspect() introspection = schema.introspect()
assert '__schema' in introspection assert '__schema' in introspection

View File

@ -34,18 +34,34 @@ class Human(Character):
class Meta: class Meta:
type_name = 'core_Human' type_name = 'core_Human'
@property
def readonly_prop(self):
return 'readonly'
@property
def write_prop(self):
return self._write_prop
@write_prop.setter
def write_prop(self, value):
self._write_prop = value
schema = Schema() schema = Schema()
def test_interface(): def test_interface():
object_type = Character.internal_type(schema) object_type = schema.T(Character)
assert Character._meta.interface is True assert Character._meta.is_interface is True
assert isinstance(object_type, GraphQLInterfaceType) assert isinstance(object_type, GraphQLInterfaceType)
assert Character._meta.type_name == 'core_Character' assert Character._meta.type_name == 'core_Character'
assert object_type.description == 'Character description' assert object_type.description == 'Character description'
assert list(object_type.get_fields().keys()) == ['name'] assert list(object_type.get_fields().keys()) == ['name']
# assert object_type.get_fields() == {
# 'name': Character._meta.fields_map['name'].internal_field(schema)}
def test_interface_cannot_initialize():
with raises(Exception) as excinfo:
c = Character()
assert 'An interface cannot be initialized' == str(excinfo.value)
def test_interface_resolve_type(): def test_interface_resolve_type():
@ -54,18 +70,39 @@ def test_interface_resolve_type():
def test_object_type(): def test_object_type():
object_type = Human.internal_type(schema) object_type = schema.T(Human)
assert Human._meta.interface is False assert Human._meta.is_interface is False
assert Human._meta.type_name == 'core_Human' assert Human._meta.type_name == 'core_Human'
assert isinstance(object_type, GraphQLObjectType) assert isinstance(object_type, GraphQLObjectType)
assert object_type.description == 'Human description' assert object_type.description == 'Human description'
assert list(object_type.get_fields().keys()) == ['name', 'friends'] assert list(object_type.get_fields().keys()) == ['name', 'friends']
# assert object_type.get_fields() == {'name': Human._meta.fields_map['name'].internal_field( # assert object_type.get_fields() == {'name': Human._meta.fields_map['name'].internal_field(
# schema), 'friends': Human._meta.fields_map['friends'].internal_field(schema)} # schema), 'friends': Human._meta.fields_map['friends'].internal_field(schema)}
assert object_type.get_interfaces() == [Character.internal_type(schema)] assert object_type.get_interfaces() == [schema.T(Character)]
assert Human._meta.fields_map['name'].object_type == Human assert Human._meta.fields_map['name'].object_type == Human
def test_object_type_container():
h = Human(name='My name')
assert h.name == 'My name'
def test_object_type_set_properties():
h = Human(readonly_prop='custom', write_prop='custom')
assert h.readonly_prop == 'readonly'
assert h.write_prop == 'custom'
def test_object_type_container_invalid_kwarg():
with raises(TypeError):
Human(invalid='My name')
def test_object_type_container_too_many_args():
with raises(IndexError):
Human('Peter', 'No friends :(', None)
def test_field_clashes(): def test_field_clashes():
with raises(Exception) as excinfo: with raises(Exception) as excinfo:
class Droid(Character): class Droid(Character):

View File

@ -1,8 +1,7 @@
SECRET_KEY = 1 SECRET_KEY = 1
INSTALLED_APPS = [ INSTALLED_APPS = [
'graphene.contrib.django', 'examples.starwars_django',
'tests.starwars_django',
'tests.contrib_django', 'tests.contrib_django',
] ]

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

@ -0,0 +1,84 @@
from graphql.core.type import (
GraphQLInputObjectField
)
import graphene
from graphene import relay
from graphene.core.types import InputObjectType
from graphene.core.schema import Schema
my_id = 0
class Query(graphene.ObjectType):
base = graphene.StringField()
class ChangeNumber(relay.ClientIDMutation):
'''Result mutation'''
class Input:
to = graphene.IntField()
result = graphene.StringField()
@classmethod
def mutate_and_get_payload(cls, input, info):
global my_id
my_id = input.get('to', my_id + 1)
return ChangeNumber(result=my_id)
class MyResultMutation(graphene.ObjectType):
change_number = graphene.Field(ChangeNumber)
schema = Schema(query=Query, mutation=MyResultMutation)
def test_mutation_input():
assert ChangeNumber.input_type
assert ChangeNumber.input_type._meta.type_name == 'ChangeNumberInput'
assert list(ChangeNumber.input_type._meta.fields_map.keys()) == ['input']
_input = ChangeNumber.input_type._meta.fields_map['input']
inner_type = _input.get_object_type(schema)
client_mutation_id_field = inner_type._meta.fields_map['client_mutation_id']
assert issubclass(inner_type, InputObjectType)
assert isinstance(client_mutation_id_field, graphene.StringField)
assert client_mutation_id_field.object_type == inner_type
assert isinstance(client_mutation_id_field.internal_field(schema), GraphQLInputObjectField)
def test_execute_mutations():
query = '''
mutation M{
first: changeNumber(input: {clientMutationId: "mutation1"}) {
clientMutationId
result
},
second: changeNumber(input: {clientMutationId: "mutation2"}) {
clientMutationId
result
}
third: changeNumber(input: {clientMutationId: "mutation3", to: 5}) {
result
clientMutationId
}
}
'''
expected = {
'first': {
'clientMutationId': 'mutation1',
'result': '1',
},
'second': {
'clientMutationId': 'mutation2',
'result': '2',
},
'third': {
'clientMutationId': 'mutation3',
'result': '5',
}
}
result = schema.execute(query, root=object())
assert not result.errors
assert result.data == expected

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,96 +0,0 @@
from collections import namedtuple
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',
)
vader = Human(
id='1001',
name='Darth Vader',
friends=['1004'],
appearsIn=[4, 5, 6],
homePlanet='Tatooine',
)
han = Human(
id='1002',
name='Han Solo',
friends=['1000', '1003', '2001'],
appearsIn=[4, 5, 6],
homePlanet=None,
)
leia = Human(
id='1003',
name='Leia Organa',
friends=['1000', '1002', '2000', '2001'],
appearsIn=[4, 5, 6],
homePlanet='Alderaan',
)
tarkin = Human(
id='1004',
name='Wilhuff Tarkin',
friends=['1001'],
appearsIn=[4],
homePlanet=None,
)
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'],
appearsIn=[4, 5, 6],
primaryFunction='Protocol',
)
artoo = Droid(
id='2001',
name='R2-D2',
friends=['1000', '1002', '1003'],
appearsIn=[4, 5, 6],
primaryFunction='Astromech',
)
droidData = {
'2000': threepio,
'2001': artoo,
}
def getCharacter(id):
return humanData.get(id) or droidData.get(id)
def getFriends(character):
return map(getCharacter, character.friends)
def getHero(episode):
if episode == 5:
return luke
return artoo
def getHuman(id):
return humanData.get(id)
def getDroid(id):
return droidData.get(id)

View File

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

@ -1,54 +0,0 @@
import graphene
from graphene import resolve_only_args, relay
from .data import (
getFaction,
getShip,
getRebels,
getEmpire,
)
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))
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):
return Faction(getFaction(id))
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.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)

13
tests/utils/test_misc.py Normal file
View File

@ -0,0 +1,13 @@
import collections
from graphql.core.type import GraphQLEnumType
from graphene.utils.misc import enum_to_graphql_enum
item = collections.namedtuple('type', 'name value')
class MyCustomEnum(list):
__name__ = 'MyName'
def test_enum_to_graphql_enum():
assert isinstance(enum_to_graphql_enum(MyCustomEnum([item('k', 'v')])), GraphQLEnumType)

View File

@ -0,0 +1,52 @@
from py.test import raises
from graphene.utils import ProxySnakeDict
def test_proxy_snake_dict():
my_data = {'one': 1, 'two': 2, 'none': None, 'threeOrFor': 3, 'inside': {'otherCamelCase': 3}}
p = ProxySnakeDict(my_data)
assert 'one' in p
assert 'two' in p
assert 'threeOrFor' in p
assert 'none' in p
assert len(p) == len(my_data)
assert p['none'] is None
assert p.get('none') is None
assert p.get('none_existent') is None
assert 'three_or_for' in p
assert p.get('three_or_for') == 3
assert 'inside' in p
assert 'other_camel_case' in p['inside']
assert sorted(p.items()) == sorted(list([('inside', ProxySnakeDict({'other_camel_case': 3})),
('none', None),
('three_or_for', 3),
('two', 2),
('one', 1)]))
def test_proxy_snake_dict_as_kwargs():
my_data = {'myData': 1}
p = ProxySnakeDict(my_data)
def func(**kwargs):
return kwargs.get('my_data')
assert func(**p) == 1
def test_proxy_snake_dict_repr():
my_data = {'myData': 1}
p = ProxySnakeDict(my_data)
assert repr(p) == "<ProxySnakeDict {'my_data': 1}>"
def test_proxy_snake_dict_set():
p = ProxySnakeDict({})
with raises(TypeError):
p['a'] = 2
def test_proxy_snake_dict_delete():
p = ProxySnakeDict({})
with raises(TypeError):
del p['a']

View File

@ -0,0 +1,14 @@
from graphene.utils import to_snake_case, to_camel_case
def test_snake_case():
assert to_snake_case('snakesOnAPlane') == 'snakes_on_a_plane'
assert to_snake_case('SnakesOnAPlane') == 'snakes_on_a_plane'
assert to_snake_case('snakes_on_a_plane') == 'snakes_on_a_plane'
assert to_snake_case('IPhoneHysteria') == 'i_phone_hysteria'
assert to_snake_case('iPhoneHysteria') == 'i_phone_hysteria'
def test_camel_case():
assert to_camel_case('snakes_on_a_plane') == 'snakesOnAPlane'
assert to_camel_case('i_phone_hysteria') == 'iPhoneHysteria'

View File

@ -14,7 +14,7 @@ deps=
setenv = setenv =
PYTHONPATH = .:{envdir} PYTHONPATH = .:{envdir}
commands= commands=
py.test py.test tests/
[pytest] [pytest]
DJANGO_SETTINGS_MODULE = tests.django_settings DJANGO_SETTINGS_MODULE = tests.django_settings