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
target/
/tests/django.sqlite

View File

@ -6,9 +6,10 @@ python:
- 3.4
- 3.5
- pypy
cache: pip
install:
- pip install pytest pytest-cov coveralls flake8 six blinker pytest-django
- pip install -e .[django]
- pip install --cache-dir $HOME/.cache/pip pytest pytest-cov coveralls flake8 six blinker pytest-django
- pip install --cache-dir $HOME/.cache/pip -e .[django]
- python setup.py develop
script:
- 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.
* **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
* **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
@ -13,29 +16,26 @@ For instaling graphene, just run this command in your shell
```bash
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:
### 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 +44,20 @@ 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](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

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.
It maps the models/fields to internal GraphQL objects without effort.
Including automatic `Django models`_ conversion.
Graphene is a Python library for building GraphQL schemas/types fast and
easily. \* **Easy to use:** Graphene helps you use GraphQL in Python
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
------------
@ -13,89 +20,49 @@ For instaling graphene, just run this command in your shell
.. code:: bash
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:
Schema definition
~~~~~~~~~~~~~~~~~
Here is one example for get you started:
.. 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):
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)
Querying
~~~~~~~~
Querying ``graphene.Schema`` is as simple as:
Then Querying ``graphene.Schema`` is as simple as:
.. code:: 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 <examples/>`__:
Graphene also supports Relay, check the `Starwars Relay example`_!
.. code:: python
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 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()
- **Basic Schema**: `Starwars example <examples/starwars>`__
- **Relay Schema**: `Starwars Relay
example <examples/starwars_relay>`__
- **Django model mapping**: `Starwars Django
example <examples/starwars_django>`__
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
.. _Django models: #djangorelay-schema
.. _Starwars Relay example: tests/starwars_relay
.. _Starwars Django example: tests/starwars_django
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master
: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
: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
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):
@ -47,20 +41,17 @@ class Query(graphene.ObjectType):
id=graphene.Argument(graphene.String)
)
class Meta:
type_name = 'core_Query'
@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 ..schema import Schema, Query
from graphql.core import graphql
from ..data import setup
setup()
def test_hero_name_query():
query = '''

View File

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import pytest
from graphql.core import graphql
from .models import *
from .schema import schema
from .data import initialize
from ..models import *
from ..schema import schema
from ..data import initialize
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
from pytest import raises
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

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 graphql.core import graphql
from ..schema import schema
from ..data import setup
from .schema import schema
setup()
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 graphql.core import graphql
from ..schema import schema
from ..data import setup
from .schema import schema
setup()
def test_correctly_fetches_id_name_rebels():

View File

@ -14,7 +14,8 @@ from graphene.core.schema import (
from graphene.core.types import (
ObjectType,
Interface
Interface,
Mutation,
)
from graphene.core.fields import (
@ -31,3 +32,8 @@ from graphene.core.fields import (
from graphene.decorators import (
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.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.contrib.django.utils import get_type_for_model, lazy_map
@ -25,7 +23,6 @@ class LazyListField(ListField):
class ConnectionOrListField(LazyField):
@memoize
def get_field(self, schema):
model_field = self.field_type
field_object_type = model_field.get_object_type(schema)
@ -52,7 +49,6 @@ class DjangoModelField(Field):
))
return _type(resolved)
@memoize
def internal_type(self, schema):
_type = self.get_object_type(schema)
if not _type and self.object_type._meta.only_fields:
@ -66,7 +62,7 @@ class DjangoModelField(Field):
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):
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)
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

@ -2,10 +2,9 @@ from django.db import models
from django.db.models.query import QuerySet
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):
schema = schema
types = schema.types.values()

View File

@ -1,5 +1,10 @@
import inspect
import six
try:
from enum import Enum
except ImportError:
class Enum(object):
pass
from functools import total_ordering, wraps
from graphql.core.type import (
GraphQLField,
@ -11,9 +16,10 @@ from graphql.core.type import (
GraphQLID,
GraphQLArgument,
GraphQLFloat,
GraphQLInputObjectField,
)
from graphene.utils import memoize, to_camel_case
from graphene.core.types import BaseObjectType
from graphene.utils import to_camel_case, ProxySnakeDict, enum_to_graphql_enum
from graphene.core.types import BaseObjectType, InputObjectType
from graphene.core.scalars import GraphQLSkipField
@ -27,7 +33,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
@ -37,9 +43,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)
@ -51,20 +61,18 @@ class Field(object):
cls._meta.add_field(self)
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:
return resolve_fn(instance, args, info)
return resolve_fn(instance, ProxySnakeDict(args), info)
else:
if isinstance(instance, BaseObjectType):
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)
return getattr(instance, self.field_name, self.get_default())
@memoize
def get_resolve_fn(self):
if self.resolve_fn:
def get_resolve_fn(self, schema):
object_type = self.get_object_type(schema)
if object_type and object_type._meta.is_mutation:
return object_type.mutate
elif self.resolve_fn:
return self.resolve_fn
else:
custom_resolve_fn_name = 'resolve_%s' % self.field_name
@ -96,24 +104,25 @@ class Field(object):
field_type = GraphQLNonNull(field_type)
return field_type
@memoize
def internal_type(self, schema):
field_type = self.field_type
_is_class = inspect.isclass(field_type)
if isinstance(field_type, Field):
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:
object_type = self.get_object_type(schema)
if object_type:
field_type = object_type.internal_type(schema)
field_type = schema.T(object_type)
field_type = self.type_wrapper(field_type)
return field_type
@memoize
def internal_field(self, schema):
if not self.object_type:
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()
for arg_name, arg_value in self.extra_args.items():
@ -128,27 +137,44 @@ class Field(object):
','.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)
if not internal_type:
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:
description = resolve_fn.__doc__ or description
@wraps(resolve_fn)
def resolver(*args):
return self.resolve(*args)
else:
resolver = self.resolve
if issubclass(self.object_type, InputObjectType):
return GraphQLInputObjectField(
internal_type,
description=description,
)
return GraphQLField(
internal_type,
description=self.description,
args=self.args,
description=description,
args=args,
resolver=resolver,
)
def __str__(self):
""" 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):
"""
@ -189,7 +215,6 @@ class Field(object):
class LazyField(Field):
@memoize
def inner_field(self, schema):
return self.get_field(schema)

View File

@ -1,7 +1,7 @@
from graphene.utils import cached_property
from collections import OrderedDict, namedtuple
DEFAULT_NAMES = ('description', 'name', 'interface',
DEFAULT_NAMES = ('description', 'name', 'is_interface', 'is_mutation',
'type_name', 'interfaces', 'proxy')
@ -10,7 +10,8 @@ class Options(object):
def __init__(self, meta=None):
self.meta = meta
self.local_fields = []
self.interface = False
self.is_interface = False
self.is_mutation = False
self.proxy = False
self.interfaces = []
self.parents = []
@ -54,15 +55,8 @@ class Options(object):
else:
self.proxy = False
if self.interfaces != [] and self.interface:
raise Exception("A interface cannot inherit from interfaces")
del self.meta
@cached_property
def object(self):
return namedtuple(self.type_name, self.fields_map.keys())
def add_field(self, 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):
self._internal_types = {}
self._types = {}
self.mutation = mutation
self.query = query
self.name = name
@ -34,6 +35,13 @@ class Schema(object):
def __repr__(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
def query(self):
return self._query
@ -41,7 +49,14 @@ class Schema(object):
@query.setter
def query(self, 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
def executor(self):
@ -53,11 +68,11 @@ class Schema(object):
def executor(self, value):
self._executor = value
@cached_property
@property
def schema(self):
if not self._query_type:
if not self._query:
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):
self._internal_types[internal_type.name] = object_type
@ -67,6 +82,7 @@ class Schema(object):
return object_type
def get_type(self, type_name):
self.schema._build_type_map()
if type_name not in self._internal_types:
raise Exception('Type %s not found in %r' % (type_name, self))
return self._internal_types[type_name]

View File

@ -5,7 +5,9 @@ from collections import OrderedDict
from graphql.core.type import (
GraphQLObjectType,
GraphQLInterfaceType
GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLArgument
)
from graphene import signals
@ -21,7 +23,7 @@ class ObjectTypeMeta(type):
return Interface in parents
def is_mutation(cls, parents):
return Mutation in parents
return issubclass(cls, Mutation)
def __new__(cls, name, bases, attrs):
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._meta.interface = new_class.is_interface(parents)
new_class._meta.mutation = new_class.is_mutation(parents)
new_class._meta.is_interface = new_class.is_interface(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.
for obj_name, obj in attrs.items():
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_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._meta.parents.append(base)
if base._meta.interface:
if base._meta.is_interface:
new_class._meta.interfaces.append(base)
# new_class._meta.parents.extend(base._meta.parents)
@ -113,50 +129,67 @@ class ObjectTypeMeta(type):
class BaseObjectType(object):
def __new__(cls, instance=None, **kwargs):
if cls._meta.interface:
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:
instance = instance.instance
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
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)
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)
def __getattr__(self, name):
if self.instance:
return getattr(self.instance, name)
def get_field(self, field):
return getattr(self.instance, field, None)
@classmethod
def fields_as_arguments(cls, schema):
return OrderedDict([(f.field_name, GraphQLArgument(f.internal_type(schema)))
for f in cls._meta.fields])
@classmethod
def resolve_objecttype(cls, schema, instance, *_):
return instance
return instance.__class__
@classmethod
def resolve_type(cls, schema, instance, *_):
objecttype = cls.resolve_objecttype(schema, instance, *_)
return objecttype.internal_type(schema)
return schema.T(objecttype)
@classmethod
@memoize
@register_internal_type
def internal_type(cls, schema):
fields_list = cls._meta.fields
fields = lambda: OrderedDict([(f.name, f.internal_field(schema))
for f in fields_list])
if cls._meta.interface:
for f in cls._meta.fields])
if cls._meta.is_interface:
return GraphQLInterfaceType(
cls._meta.type_name,
description=cls._meta.description,
@ -167,19 +200,34 @@ class BaseObjectType(object):
return GraphQLObjectType(
cls._meta.type_name,
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,
is_type_of=getattr(cls, 'is_type_of', None)
)
class Interface(six.with_metaclass(ObjectTypeMeta, BaseObjectType)):
pass
class ObjectType(six.with_metaclass(ObjectTypeMeta, BaseObjectType)):
pass
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)):
pass
class InputObjectType(ObjectType):
@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,
PageInfo,
Edge,
Connection
Connection,
ClientIDMutation
)
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.utils import memoize
class ConnectionField(Field):
@ -51,24 +50,21 @@ class ConnectionField(Field):
connection.set_connection_data(resolved)
return connection
@memoize
def get_connection_type(self, node):
connection_type = self.connection_type or node.get_connection_type()
edge_type = self.get_edge_type(node)
return connection_type.for_node(node, edge_type=edge_type)
@memoize
def get_edge_type(self, node):
return self.edge_type or node.get_edge_type()
@memoize
def internal_type(self, schema):
from graphene.relay.utils import is_node
node = self.get_object_type(schema)
assert is_node(node), 'Only nodes have connections.'
schema.register(node)
return self.get_connection_type(node).internal_type(schema)
connection_type = self.get_connection_type(node)
return schema.T(connection_type)
class NodeField(Field):

View File

@ -2,7 +2,7 @@ from graphql_relay.node.node import (
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.relay.fields import GlobalIDField
from graphene.utils import memoize
@ -84,3 +84,32 @@ class BaseNode(object):
class Node(BaseNode, Interface):
'''An object with an ID'''
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(
name='graphene',
version='0.1.6.1',
version='0.3.0',
description='Graphene: Python DSL for GraphQL',
long_description=open('README.rst').read(),

View File

@ -46,12 +46,12 @@ schema = Schema()
def test_django_interface():
assert DjangoNode._meta.interface is True
assert DjangoNode._meta.is_interface is True
def test_pseudo_interface():
object_type = Character.internal_type(schema)
assert Character._meta.interface is True
object_type = schema.T(Character)
assert Character._meta.is_interface is True
assert isinstance(object_type, GraphQLInterfaceType)
assert Character._meta.model == Reporter
assert_equal_lists(
@ -81,9 +81,9 @@ def test_interface_resolve_type():
def test_object_type():
object_type = Human.internal_type(schema)
object_type = schema.T(Human)
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_equal_lists(
object_type.get_fields().keys(),
@ -95,9 +95,9 @@ def test_object_type():
# 'reporter': fields_map['reporter'].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():
assert Human._meta.interface is False
assert Human._meta.is_interface is False
assert DjangoNode in Human._meta.interfaces

View File

@ -24,19 +24,14 @@ from graphene.core.types import ObjectType
class ObjectType(object):
_meta = Options()
def resolve(self, *args, **kwargs):
def resolve_customdoc(self, *args, **kwargs):
'''Resolver documentation'''
return None
def can_resolve(self, *args):
return True
def __str__(self):
return "ObjectType"
ot = ObjectType()
ObjectType._meta.contribute_to_class(ObjectType, '_meta')
ot = ObjectType
schema = Schema()
@ -89,7 +84,7 @@ def test_field_resolve():
f = StringField(required=True, resolve=lambda *args: 'RESOLVED')
f.contribute_to_class(ot, 'field_name')
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():
@ -172,3 +167,10 @@ def test_field_repr_contributed():
f = StringField()
f.contribute_to_class(ot, '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:
interface = True
is_interface = True
type_name = 'Character'

View File

@ -46,11 +46,11 @@ class Human(Character):
schema = Schema()
Human_type = Human.internal_type(schema)
Human_type = schema.T(Human)
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():

View File

@ -125,9 +125,23 @@ def test_schema_register():
class MyType(ObjectType):
type = StringField(resolve=lambda *_: 'Dog')
schema.query = 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():
schema = Schema(name='My own schema')
@ -138,4 +152,3 @@ def test_schema_introspect():
introspection = schema.introspect()
assert '__schema' in introspection

View File

@ -34,18 +34,34 @@ class Human(Character):
class Meta:
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()
def test_interface():
object_type = Character.internal_type(schema)
assert Character._meta.interface is True
object_type = schema.T(Character)
assert Character._meta.is_interface is True
assert isinstance(object_type, GraphQLInterfaceType)
assert Character._meta.type_name == 'core_Character'
assert object_type.description == 'Character description'
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():
@ -54,18 +70,39 @@ def test_interface_resolve_type():
def test_object_type():
object_type = Human.internal_type(schema)
assert Human._meta.interface is False
object_type = schema.T(Human)
assert Human._meta.is_interface is False
assert Human._meta.type_name == 'core_Human'
assert isinstance(object_type, GraphQLObjectType)
assert object_type.description == 'Human description'
assert list(object_type.get_fields().keys()) == ['name', 'friends']
# assert object_type.get_fields() == {'name': Human._meta.fields_map['name'].internal_field(
# 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
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():
with raises(Exception) as excinfo:
class Droid(Character):

View File

@ -1,8 +1,7 @@
SECRET_KEY = 1
INSTALLED_APPS = [
'graphene.contrib.django',
'tests.starwars_django',
'examples.starwars_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():
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 (
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,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 =
PYTHONPATH = .:{envdir}
commands=
py.test
py.test tests/
[pytest]
DJANGO_SETTINGS_MODULE = tests.django_settings