mirror of
https://github.com/graphql-python/graphene.git
synced 2024-11-25 11:03:58 +03:00
First working version of Graphene 😃
This commit is contained in:
commit
931d0ddb1c
61
.gitignore
vendored
Normal file
61
.gitignore
vendored
Normal 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
15
.travis.yml
Normal 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
21
LICENSE
Normal 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
21
README.md
Normal 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
28
graphene/__init__.py
Normal 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
|
||||||
|
)
|
0
graphene/core/__init__.py
Normal file
0
graphene/core/__init__.py
Normal file
136
graphene/core/fields.py
Normal file
136
graphene/core/fields.py
Normal 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
69
graphene/core/options.py
Normal 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
136
graphene/core/types.py
Normal 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
8
graphene/decorators.py
Normal 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
|
0
graphene/relay/__init__.py
Normal file
0
graphene/relay/__init__.py
Normal file
16
graphene/utils.py
Normal file
16
graphene/utils.py
Normal 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
3
setup.cfg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[flake8]
|
||||||
|
exclude = tests/*,setup.py
|
||||||
|
max-line-length = 160
|
62
setup.py
Normal file
62
setup.py
Normal 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
61
tests/core/test_fields.py
Normal 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)
|
67
tests/core/test_options.py
Normal file
67
tests/core/test_options.py
Normal 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
41
tests/core/test_types.py
Normal 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]
|
0
tests/starwars/__init__.py
Normal file
0
tests/starwars/__init__.py
Normal file
95
tests/starwars/data.py
Normal file
95
tests/starwars/data.py
Normal 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
69
tests/starwars/schema.py
Normal 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)
|
345
tests/starwars/test_query.py
Normal file
345
tests/starwars/test_query.py
Normal 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
|
Loading…
Reference in New Issue
Block a user