First working version of Graphene 😃

This commit is contained in:
Syrus Akbary 2015-09-24 02:11:50 -07:00
commit 931d0ddb1c
22 changed files with 1266 additions and 0 deletions

61
.gitignore vendored Normal file
View File

@ -0,0 +1,61 @@
# Created by https://www.gitignore.io
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/

15
.travis.yml Normal file
View File

@ -0,0 +1,15 @@
language: python
sudo: false
python:
- 2.7
install:
- pip install pytest pytest-cov coveralls flake8
- pip install -e .[django]
- pip install git+https://github.com/dittos/graphqllib.git # Last version of graphqllib
- pip install graphql-relay
- python setup.py develop
script:
- py.test --cov=graphene
# - flake8
after_success:
- coveralls

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Syrus Akbary
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

21
README.md Normal file
View File

@ -0,0 +1,21 @@
# Graphene: GraphQL Object Mapper
This is a library to use GraphQL in Python in a easy way.
It will map the models/fields to internal GraphQL-py objects without effort.
[![Build Status](https://travis-ci.org/syrusakbary/graphene.svg?branch=master)](https://travis-ci.org/syrusakbary/graphene)
[![Coverage Status](https://coveralls.io/repos/syrusakbary/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/syrusakbary/graphene?branch=master)
## Contributing
After cloning this repo, ensure dependencies are installed by running:
```sh
python setup.py install
```
After developing, the full test suite can be evaluated by running:
```sh
python setup.py test # Use --pytest-args="-v -s" for verbose mode
```

28
graphene/__init__.py Normal file
View File

@ -0,0 +1,28 @@
from graphql.core.type import (
GraphQLEnumType as Enum,
GraphQLArgument as Argument,
# GraphQLSchema as Schema,
GraphQLString as String,
GraphQLInt as Int,
GraphQLID as ID
)
from graphene.core.fields import (
Field,
StringField,
IntField,
BooleanField,
IDField,
ListField,
NonNullField,
)
from graphene.core.types import (
ObjectType,
Interface,
Schema
)
from graphene.decorators import (
resolve_only_args
)

View File

136
graphene/core/fields.py Normal file
View File

@ -0,0 +1,136 @@
import inspect
from graphql.core.type import (
GraphQLField,
GraphQLList,
GraphQLNonNull,
GraphQLInt,
GraphQLString,
GraphQLBoolean,
GraphQLID,
GraphQLArgument,
)
from graphene.core.types import ObjectType, Interface
from graphene.utils import cached_property
class Field(object):
def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args):
self.field_type = field_type
self.resolve_fn = resolve
self.null = null
self.args = args or {}
self.extra_args = extra_args
self._type = None
self.description = description or self.__doc__
self.object_type = None
def contribute_to_class(self, cls, name):
self.field_name = name
self.object_type = cls
if isinstance(self.field_type, Field) and not self.field_type.object_type:
self.field_type.contribute_to_class(cls, name)
cls._meta.add_field(self)
def resolver(self, instance, args, info):
if self.object_type.can_resolve(self.field_name, instance, args, info):
return self.resolve(instance, args, info)
else:
return None
def resolve(self, instance, args, info):
if self.resolve_fn:
resolve_fn = self.resolve_fn
else:
resolve_fn = lambda root, args, info: root.resolve(self.field_name, args, info)
return resolve_fn(instance, args, info)
@cached_property
def type(self):
field_type = self.field_type
_is_class = inspect.isclass(field_type)
if _is_class and issubclass(field_type, ObjectType):
field_type = field_type._meta.type
elif isinstance(field_type, Field):
field_type = field_type.type
elif field_type == 'self':
field_type = self.object_type._meta.type
field_type = self.type_wrapper(field_type)
return field_type
def type_wrapper(self, field_type):
if not self.null:
field_type = GraphQLNonNull(field_type)
return field_type
@cached_property
def field(self):
if not self.field_type:
raise Exception('Must specify a field GraphQL type for the field %s'%self.field_name)
if not self.object_type:
raise Exception('Field could not be constructed in a non graphene.Type or graphene.Interface')
extra_args = self.extra_args.copy()
for arg_name, arg_value in extra_args.items():
if isinstance(arg_value, GraphQLArgument):
self.args[arg_name] = arg_value
del extra_args[arg_name]
if extra_args != {}:
raise TypeError("Field %s.%s initiated with invalid args: %s" % (
self.object_type,
self.field_name,
','.join(meta_attrs.keys())
))
return GraphQLField(
self.type,
description=self.description,
args=self.args,
resolver=self.resolver,
)
def __str__(self):
""" Return "object_type.field_name". """
return '%s.%s' % (self.object_type, self.field_name)
def __repr__(self):
"""
Displays the module, class and name of the field.
"""
path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
name = getattr(self, 'field_name', None)
if name is not None:
return '<%s: %s>' % (path, name)
return '<%s>' % path
class TypeField(Field):
def __init__(self, *args, **kwargs):
super(TypeField, self).__init__(self.field_type, *args, **kwargs)
class StringField(TypeField):
field_type = GraphQLString
class IntField(TypeField):
field_type = GraphQLInt
class BooleanField(TypeField):
field_type = GraphQLBoolean
class IDField(TypeField):
field_type = GraphQLID
class ListField(Field):
def type_wrapper(self, field_type):
return GraphQLList(field_type)
class NonNullField(Field):
def type_wrapper(self, field_type):
return GraphQLNonNull(field_type)

69
graphene/core/options.py Normal file
View File

@ -0,0 +1,69 @@
from graphene.utils import cached_property
DEFAULT_NAMES = ('description', 'name', 'interface', 'type_name', 'interfaces', 'proxy')
class Options(object):
def __init__(self, meta=None):
self.meta = meta
self.local_fields = []
self.interface = False
self.proxy = False
self.interfaces = []
self.parents = []
def contribute_to_class(self, cls, name):
cls._meta = self
self.parent = cls
# First, construct the default values for these options.
self.object_name = cls.__name__
self.type_name = self.object_name
self.description = cls.__doc__
# Store the original user-defined values for each option,
# for use when serializing the model definition
self.original_attrs = {}
# Next, apply any overridden values from 'class Meta'.
if self.meta:
meta_attrs = self.meta.__dict__.copy()
for name in self.meta.__dict__:
# Ignore any private attributes that Django doesn't care about.
# NOTE: We can't modify a dictionary's contents while looping
# over it, so we loop over the *original* dictionary instead.
if name.startswith('_'):
del meta_attrs[name]
for attr_name in DEFAULT_NAMES:
if attr_name in meta_attrs:
setattr(self, attr_name, meta_attrs.pop(attr_name))
self.original_attrs[attr_name] = getattr(self, attr_name)
elif hasattr(self.meta, attr_name):
setattr(self, attr_name, getattr(self.meta, attr_name))
self.original_attrs[attr_name] = getattr(self, attr_name)
# Any leftover attributes must be invalid.
if meta_attrs != {}:
raise TypeError("'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys()))
if self.interfaces != [] and self.interface:
raise Exception("A interface cannot inherit from interfaces")
del self.meta
def add_field(self, field):
self.local_fields.append(field)
setattr(self.parent, field.field_name, field)
@cached_property
def fields(self):
fields = []
for parent in self.parents:
fields.extend(parent._meta.fields)
return self.local_fields + fields
@cached_property
def fields_map(self):
return {f.field_name:f for f in self.fields}
@cached_property
def type(self):
return self.parent.get_graphql_type()

136
graphene/core/types.py Normal file
View File

@ -0,0 +1,136 @@
import inspect
import six
from graphql.core.type import (
GraphQLObjectType,
GraphQLInterfaceType,
GraphQLSchema
)
from graphql.core import graphql
from graphene.core.options import Options
class ObjectTypeMeta(type):
def __new__(cls, name, bases, attrs):
super_new = super(ObjectTypeMeta, cls).__new__
parents = [b for b in bases if isinstance(b, ObjectTypeMeta)]
if not parents:
# If this isn't a subclass of Model, don't do anything special.
return super_new(cls, name, bases, attrs)
module = attrs.pop('__module__')
doc = attrs.pop('__doc__', None)
new_class = super_new(cls, name, bases, {'__module__': module, '__doc__': doc})
attr_meta = attrs.pop('Meta', None)
if not attr_meta:
meta = getattr(new_class, 'Meta', None)
else:
meta = attr_meta
base_meta = getattr(new_class, '_meta', None)
new_class.add_to_class('_meta', Options(meta))
if base_meta and base_meta.proxy:
new_class._meta.interface = base_meta.interface
# Add all attributes to the class.
for obj_name, obj in attrs.items():
new_class.add_to_class(obj_name, obj)
new_fields = new_class._meta.local_fields
field_names = {f.field_name for f in new_fields}
for base in parents:
original_base = base
if not hasattr(base, '_meta'):
# Things without _meta aren't functional models, so they're
# uninteresting parents.
continue
parent_fields = base._meta.local_fields
# Check for clashes between locally declared fields and those
# on the base classes (we cannot handle shadowed fields at the
# moment).
for field in parent_fields:
if field.field_name in field_names:
raise FieldError(
'Local field %r in class %r clashes '
'with field of similar name from '
'base class %r' % (field.field_name, name, base.__name__)
)
new_class._meta.parents.append(base)
if base._meta.interface:
new_class._meta.interfaces.append(base)
# new_class._meta.parents.extend(base._meta.parents)
return new_class
def add_to_class(cls, name, value):
# We should call the contribute_to_class method only if it's bound
if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'):
value.contribute_to_class(cls, name)
else:
setattr(cls, name, value)
class ObjectType(six.with_metaclass(ObjectTypeMeta)):
def __init__(self, instance=None):
self.instance = instance
def get_field(self, field):
return getattr(self.instance, field, None)
def resolve(self, field_name, args, info):
if field_name not in self._meta.fields_map.keys():
raise Exception('Field %s not found in model'%field_name)
custom_resolve_fn = 'resolve_%s'%field_name
if hasattr(self, custom_resolve_fn):
resolve_fn = getattr(self, custom_resolve_fn)
return resolve_fn(args, info)
return self.get_field(field_name)
@classmethod
def can_resolve(cls, field_name, instance, args, info):
# Useful for manage permissions in fields
return True
@classmethod
def resolve_type(cls, instance, *_):
return instance._meta.type
@classmethod
def get_graphql_type(cls):
fields = cls._meta.fields_map
if cls._meta.interface:
return GraphQLInterfaceType(
cls._meta.type_name,
description=cls._meta.description,
resolve_type=cls.resolve_type,
fields=lambda: {name:field.field for name, field in fields.items()}
)
return GraphQLObjectType(
cls._meta.type_name,
description=cls._meta.description,
interfaces=[i._meta.type for i in cls._meta.interfaces],
fields=lambda: {name:field.field for name, field in fields.items()}
)
class Interface(ObjectType):
class Meta:
interface = True
proxy = True
class Schema(object):
def __init__(self, query, mutation=None):
self.query = query
self.query_type = query._meta.type
self._schema = GraphQLSchema(query=self.query_type, mutation=mutation)
def execute(self, request='', root=None, vars=None, operation_name=None):
return graphql(
self._schema,
request=request,
root=root or self.query(),
vars=vars,
operation_name=operation_name
)

8
graphene/decorators.py Normal file
View File

@ -0,0 +1,8 @@
from functools import wraps
def resolve_only_args(func):
@wraps(func)
def inner(self, args, info):
return func(self, **args)
return inner

View File

16
graphene/utils.py Normal file
View File

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

3
setup.cfg Normal file
View File

@ -0,0 +1,3 @@
[flake8]
exclude = tests/*,setup.py
max-line-length = 160

62
setup.py Normal file
View File

@ -0,0 +1,62 @@
import sys
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
class PyTest(TestCommand):
user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]
def initialize_options(self):
TestCommand.initialize_options(self)
self.pytest_args = []
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = []
self.test_suite = True
def run_tests(self):
#import here, cause outside the eggs aren't loaded
import pytest
errno = pytest.main(self.pytest_args)
sys.exit(errno)
setup(
name='graphene',
version='0.1',
description='Graphene: GraphQL Object Mapper',
url='https://github.com/syrusakbary/graphene',
author='Syrus Akbary',
author_email='me@syrusakbary.com',
license='MIT',
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries',
'Programming Language :: Python :: 2',
],
keywords='api graphql protocol rest relay graphene',
packages=find_packages(exclude=['tests']),
install_requires=[
'graphqllib',
'graphql-relay'
],
tests_require=['pytest>=2.7.2'],
extras_require={
'django': [
'Django>=1.8.0,<1.9',
'singledispatch>=3.4.0.3',
],
},
cmdclass={'test': PyTest},
)

61
tests/core/test_fields.py Normal file
View File

@ -0,0 +1,61 @@
from py.test import raises
from collections import namedtuple
from pytest import raises
from graphene.core.fields import (
Field,
StringField,
)
from graphene.core.options import Options
from graphql.core.type import (
GraphQLField,
GraphQLNonNull,
GraphQLInt,
GraphQLString,
GraphQLBoolean,
GraphQLID,
)
class ObjectType(object):
_meta = Options()
def resolve(self, *args, **kwargs):
return None
def can_resolve(self, *args):
return True
ot = ObjectType()
ObjectType._meta.contribute_to_class(ObjectType, '_meta')
def test_field_no_contributed_raises_error():
f = Field(GraphQLString)
with raises(Exception) as excinfo:
f.field
def test_field_type():
f = Field(GraphQLString)
f.contribute_to_class(ot, 'field_name')
assert isinstance(f.field, GraphQLField)
assert f.type == GraphQLString
def test_stringfield_type():
f = StringField()
f.contribute_to_class(ot, 'field_name')
assert f.type == GraphQLString
def test_stringfield_type_null():
f = StringField(null=False)
f.contribute_to_class(ot, 'field_name')
assert isinstance(f.field, GraphQLField)
assert isinstance(f.type, GraphQLNonNull)
def test_field_resolve():
f = StringField(null=False)
f.contribute_to_class(ot, 'field_name')
field_type = f.field
field_type.resolver(ot,2,3)

View File

@ -0,0 +1,67 @@
from py.test import raises
from collections import namedtuple
from pytest import raises
from graphene.core.fields import (
Field,
StringField,
)
from graphene.core.options import Options
class Meta:
interface = True
type_name = 'Character'
class InvalidMeta:
other_value = True
def test_field_added_in_meta():
opt = Options(Meta)
class ObjectType(object):
pass
opt.contribute_to_class(ObjectType, '_meta')
f = StringField()
f.field_name = 'string_field'
opt.add_field(f)
assert f in opt.fields
def test_options_contribute():
opt = Options(Meta)
class ObjectType(object):
pass
opt.contribute_to_class(ObjectType, '_meta')
assert ObjectType._meta == opt
def test_options_typename():
opt = Options(Meta)
class ObjectType(object):
pass
opt.contribute_to_class(ObjectType, '_meta')
assert opt.type_name == 'Character'
def test_options_description():
opt = Options(Meta)
class ObjectType(object):
'''False description'''
pass
opt.contribute_to_class(ObjectType, '_meta')
assert opt.description == 'False description'
def test_field_no_contributed_raises_error():
opt = Options(InvalidMeta)
class ObjectType(object):
pass
with raises(Exception) as excinfo:
opt.contribute_to_class(ObjectType, '_meta')
assert 'invalid attribute' in str(excinfo.value)

41
tests/core/test_types.py Normal file
View File

@ -0,0 +1,41 @@
from py.test import raises
from collections import namedtuple
from pytest import raises
from graphene.core.fields import (
Field,
StringField,
)
from graphql.core.type import (
GraphQLObjectType,
GraphQLInterfaceType
)
from graphene.core.types import (
Interface,
ObjectType
)
class Character(Interface):
'''Character description'''
name = StringField()
class Human(Character):
'''Human description'''
friends = StringField()
def test_interface():
object_type = Character._meta.type
assert Character._meta.interface == True
assert Character._meta.type_name == 'Character'
assert isinstance(object_type, GraphQLInterfaceType)
assert object_type.description == 'Character description'
assert object_type.get_fields() == {'name': Character.name.field}
def test_object_type():
object_type = Human._meta.type
assert Human._meta.interface == False
assert Human._meta.type_name == 'Human'
assert isinstance(object_type, GraphQLObjectType)
assert object_type.description == 'Human description'
assert object_type.get_fields() == {'name': Character.name.field, 'friends': Human.friends.field}
assert object_type.get_interfaces() == [Character._meta.type]

View File

95
tests/starwars/data.py Normal file
View File

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

69
tests/starwars/schema.py Normal file
View File

@ -0,0 +1,69 @@
import graphene
from graphene import resolve_only_args
from .data import getHero, getHuman, getCharacter, getDroid, Human as _Human, Droid as _Droid
from graphql.core.type import (
GraphQLObjectType,
GraphQLField,
GraphQLString,
GraphQLNonNull,
GraphQLArgument
)
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):
id = graphene.IDField()
name = graphene.StringField()
friends = graphene.ListField(graphene.Field('self'))
appearsIn = graphene.ListField(Episode)
def resolve_friends(self, args, *_):
return [wrap_character(getCharacter(f)) for f in self.instance.friends]
class Human(Character):
homePlanet = graphene.StringField()
class Droid(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)
)
@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))
if human:
return Human(human)
@resolve_only_args
def resolve_droid(self, id):
return wrap_character(getDroid(id))
Schema = graphene.Schema(query=Query)

View File

@ -0,0 +1,345 @@
from .schema import Schema, Query
from graphql.core import graphql
def test_hero_name_query():
query = '''
query HeroNameQuery {
hero {
name
}
}
'''
expected = {
'hero': {
'name': 'R2-D2'
}
}
result = Schema.execute(query)
assert not result.errors
assert result.data == expected
def test_hero_name_and_friends_query():
query = '''
query HeroNameAndFriendsQuery {
hero {
id
name
friends {
name
}
}
}
'''
expected = {
'hero': {
'id': '2001',
'name': 'R2-D2',
'friends': [
{'name': 'Luke Skywalker'},
{'name': 'Han Solo'},
{'name': 'Leia Organa'},
]
}
}
result = Schema.execute(query)
assert not result.errors
assert result.data == expected
def test_nested_query():
query = '''
query NestedQuery {
hero {
name
friends {
name
appearsIn
friends {
name
}
}
}
}
'''
expected = {
'hero': {
'name': 'R2-D2',
'friends': [
{
'name': 'Luke Skywalker',
'appearsIn': [ 'NEWHOPE', 'EMPIRE', 'JEDI' ],
'friends': [
{
'name': 'Han Solo',
},
{
'name': 'Leia Organa',
},
{
'name': 'C-3PO',
},
{
'name': 'R2-D2',
},
]
},
{
'name': 'Han Solo',
'appearsIn': [ 'NEWHOPE', 'EMPIRE', 'JEDI' ],
'friends': [
{
'name': 'Luke Skywalker',
},
{
'name': 'Leia Organa',
},
{
'name': 'R2-D2',
},
]
},
{
'name': 'Leia Organa',
'appearsIn': [ 'NEWHOPE', 'EMPIRE', 'JEDI' ],
'friends': [
{
'name': 'Luke Skywalker',
},
{
'name': 'Han Solo',
},
{
'name': 'C-3PO',
},
{
'name': 'R2-D2',
},
]
},
]
}
}
result = Schema.execute(query)
assert not result.errors
assert result.data == expected
def test_fetch_luke_query():
query = '''
query FetchLukeQuery {
human(id: "1000") {
name
}
}
'''
expected = {
'human': {
'name': 'Luke Skywalker',
}
}
result = Schema.execute(query)
assert not result.errors
assert result.data == expected
def test_fetch_some_id_query():
query = '''
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
'''
params = {
'someId': '1000',
}
expected = {
'human': {
'name': 'Luke Skywalker',
}
}
result = Schema.execute(query, None, params)
assert not result.errors
assert result.data == expected
def test_fetch_some_id_query2():
query = '''
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
'''
params = {
'someId': '1002',
}
expected = {
'human': {
'name': 'Han Solo',
}
}
result = Schema.execute(query, None, params)
assert not result.errors
assert result.data == expected
def test_invalid_id_query():
query = '''
query humanQuery($id: String!) {
human(id: $id) {
name
}
}
'''
params = {
'id': 'not a valid id',
}
expected = {
'human': None
}
result = Schema.execute(query, None, params)
assert not result.errors
assert result.data == expected
def test_fetch_luke_aliased():
query = '''
query FetchLukeAliased {
luke: human(id: "1000") {
name
}
}
'''
expected = {
'luke': {
'name': 'Luke Skywalker',
}
}
result = Schema.execute(query)
assert not result.errors
assert result.data == expected
def test_fetch_luke_and_leia_aliased():
query = '''
query FetchLukeAndLeiaAliased {
luke: human(id: "1000") {
name
}
leia: human(id: "1003") {
name
}
}
'''
expected = {
'luke': {
'name': 'Luke Skywalker',
},
'leia': {
'name': 'Leia Organa',
}
}
result = Schema.execute(query)
assert not result.errors
assert result.data == expected
def test_duplicate_fields():
query = '''
query DuplicateFields {
luke: human(id: "1000") {
name
homePlanet
}
leia: human(id: "1003") {
name
homePlanet
}
}
'''
expected = {
'luke': {
'name': 'Luke Skywalker',
'homePlanet': 'Tatooine',
},
'leia': {
'name': 'Leia Organa',
'homePlanet': 'Alderaan',
}
}
result = Schema.execute(query)
assert not result.errors
assert result.data == expected
def test_use_fragment():
query = '''
query UseFragment {
luke: human(id: "1000") {
...HumanFragment
}
leia: human(id: "1003") {
...HumanFragment
}
}
fragment HumanFragment on Human {
name
homePlanet
}
'''
expected = {
'luke': {
'name': 'Luke Skywalker',
'homePlanet': 'Tatooine',
},
'leia': {
'name': 'Leia Organa',
'homePlanet': 'Alderaan',
}
}
result = Schema.execute(query)
assert not result.errors
assert result.data == expected
def test_check_type_of_r2():
query = '''
query CheckTypeOfR2 {
hero {
__typename
name
}
}
'''
expected = {
'hero': {
'__typename': 'Droid',
'name': 'R2-D2',
}
}
result = Schema.execute(query)
assert not result.errors
assert result.data == expected
def test_check_type_of_luke():
query = '''
query CheckTypeOfLuke {
hero(episode: EMPIRE) {
__typename
name
}
}
'''
expected = {
'hero': {
'__typename': 'Human',
'name': 'Luke Skywalker',
}
}
result = Schema.execute(query)
assert not result.errors
assert result.data == expected

12
tox.ini Normal file
View File

@ -0,0 +1,12 @@
[tox]
envlist = py27
[testenv]
deps=
pytest>=2.7.2
django>=1.8.0,<1.9
flake8
singledispatch
commands=
py.test
flake8