mirror of
https://github.com/graphql-python/graphene.git
synced 2024-11-10 19:56:45 +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