Merge branch 'master' into master

This commit is contained in:
Syrus Akbary 2016-12-18 13:12:51 -08:00 committed by GitHub
commit dda294d911
57 changed files with 1187 additions and 183 deletions

View File

@ -6,19 +6,19 @@ python:
- 3.5 - 3.5
- pypy - pypy
before_install: before_install:
- | - |
if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
export PYENV_ROOT="$HOME/.pyenv" export PYENV_ROOT="$HOME/.pyenv"
if [ -f "$PYENV_ROOT/bin/pyenv" ]; then if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
cd "$PYENV_ROOT" && git pull cd "$PYENV_ROOT" && git pull
else else
rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
fi fi
export PYPY_VERSION="4.0.1" export PYPY_VERSION="4.0.1"
"$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION" "$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION"
virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
fi fi
install: install:
- | - |
if [ "$TEST_TYPE" = build ]; then if [ "$TEST_TYPE" = build ]; then
@ -52,3 +52,10 @@ matrix:
include: include:
- python: '2.7' - python: '2.7'
env: TEST_TYPE=lint env: TEST_TYPE=lint
deploy:
provider: pypi
user: syrusakbary
on:
tags: true
password:
secure: LHOp9DvYR+70vj4YVY8+JRNCKUOfYZREEUY3+4lMUpY7Zy5QwDfgEMXG64ybREH9dFldpUqVXRj53eeU3spfudSfh8NHkgqW7qihez2AhSnRc4dK6ooNfB+kLcSoJ4nUFGxdYImABc4V1hJvflGaUkTwDNYVxJF938bPaO797IvSbuI86llwqkvuK2Vegv9q/fy9sVGaF9VZIs4JgXwR5AyDR7FBArl+S84vWww4vTFD33hoE88VR4QvFY3/71BwRtQrnCMm7AOm31P9u29yi3bpzQpiOR2rHsgrsYdm597QzFKVxYwsmf9uAx2bpbSPy2WibunLePIvOFwm8xcfwnz4/J4ONBc5PSFmUytTWpzEnxb0bfUNLuYloIS24V6OZ8BfAhiYZ1AwySeJCQDM4Vk1V8IF6trTtyx5EW/uV9jsHCZ3LFsAD7UnFRTosIgN3SAK3ZWCEk5oF2IvjecsolEfkRXB3q9EjMkkuXRUeFDH2lWJLgNE27BzY6myvZVzPmfwZUsPBlPD/6w+WLSp97Rjgr9zS3T1d4ddqFM4ZYu04f2i7a/UUQqG+itzzuX5DWLPvzuNt37JB45mB9IsvxPyXZ6SkAcLl48NGyKok1f3vQnvphkfkl4lni29woKhaau8xlsuEDrcwOoeAsVcZXiItg+l+z2SlIwM0A06EvQ=

View File

@ -83,3 +83,21 @@ After developing, the full test suite can be evaluated by running:
```sh ```sh
python setup.py test # Use --pytest-args="-v -s" for verbose mode python setup.py test # Use --pytest-args="-v -s" for verbose mode
``` ```
### Documentation
The documentation is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme.
The documentation dependencies are installed by running:
```sh
cd docs
pip install -r requirements.txt
```
Then to produce a HTML version of the documentation:
```sh
make html
```

View File

@ -1,37 +1,38 @@
Please read `UPGRADE-v1.0.md`_ to learn how to upgrade to Graphene ``1.0``. Please read `UPGRADE-v1.0.md </UPGRADE-v1.0.md>`__ to learn how to
upgrade to Graphene ``1.0``.
-------------- --------------
|Graphene Logo| `Graphene`_ |Build Status| |PyPI version| |Coverage Status| |Graphene Logo| `Graphene <http://graphene-python.org>`__ |Build Status| |PyPI version| |Coverage Status|
=========================================================================== =========================================================================================================
`Graphene`_ is a Python library for building GraphQL schemas/types fast `Graphene <http://graphene-python.org>`__ is a Python library for
and easily. building GraphQL schemas/types fast and easily.
- **Easy to use:** Graphene helps you use GraphQL in Python without - **Easy to use:** Graphene helps you use GraphQL in Python without
effort. effort.
- **Relay:** Graphene has builtin support for Relay - **Relay:** Graphene has builtin support for Relay
- **Data agnostic:** Graphene supports any kind of data source: SQL - **Data agnostic:** Graphene supports any kind of data source: SQL
(Django, SQLAlchemy), NoSQL, custom Python objects, etc. We believe that (Django, SQLAlchemy), NoSQL, custom Python objects, etc. We believe
by providing a complete API you could plug Graphene anywhere your that by providing a complete API you could plug Graphene anywhere
data lives and make your data available through GraphQL. your data lives and make your data available through GraphQL.
Integrations Integrations
------------ ------------
Graphene has multiple integrations with different frameworks: Graphene has multiple integrations with different frameworks:
+---------------------+-------------------------------------+ +---------------------+----------------------------------------------------------------------------------------------+
| integration | Package | | integration | Package |
+=====================+=====================================+ +=====================+==============================================================================================+
| Django | `graphene-django`_ | | Django | `graphene-django <https://github.com/graphql-python/graphene-django/>`__ |
+---------------------+-------------------------------------+ +---------------------+----------------------------------------------------------------------------------------------+
| SQLAlchemy | `graphene-sqlalchemy`_ | | SQLAlchemy | `graphene-sqlalchemy <https://github.com/graphql-python/graphene-sqlalchemy/>`__ |
+---------------------+-------------------------------------+ +---------------------+----------------------------------------------------------------------------------------------+
| Google App Engine | `graphene-gae`_ | | Google App Engine | `graphene-gae <https://github.com/graphql-python/graphene-gae/>`__ |
+---------------------+-------------------------------------+ +---------------------+----------------------------------------------------------------------------------------------+
| Peewee | *In progress* (`Tracking Issue`_) | | Peewee | *In progress* (`Tracking Issue <https://github.com/graphql-python/graphene/issues/289>`__) |
+---------------------+-------------------------------------+ +---------------------+----------------------------------------------------------------------------------------------+
Installation Installation
------------ ------------
@ -45,7 +46,8 @@ For instaling graphene, just run this command in your shell
1.0 Upgrade Guide 1.0 Upgrade Guide
----------------- -----------------
Please read `UPGRADE-v1.0.md`_ to learn how to upgrade. Please read `UPGRADE-v1.0.md </UPGRADE-v1.0.md>`__ to learn how to
upgrade.
Examples Examples
-------- --------
@ -74,10 +76,11 @@ Then Querying ``graphene.Schema`` is as simple as:
result = schema.execute(query) result = schema.execute(query)
If you want to learn even more, you can also check the following If you want to learn even more, you can also check the following
`examples`_: `examples <examples/>`__:
- **Basic Schema**: `Starwars example`_ - **Basic Schema**: `Starwars example <examples/starwars>`__
- **Relay Schema**: `Starwars Relay example`_ - **Relay Schema**: `Starwars Relay
example <examples/starwars_relay>`__
Contributing Contributing
------------ ------------
@ -94,15 +97,24 @@ After developing, the full test suite can be evaluated by running:
python setup.py test # Use --pytest-args="-v -s" for verbose mode python setup.py test # Use --pytest-args="-v -s" for verbose mode
.. _UPGRADE-v1.0.md: /UPGRADE-v1.0.md Documentation
.. _Graphene: http://graphene-python.org ~~~~~~~~~~~~~
.. _graphene-django: https://github.com/graphql-python/graphene-django/
.. _graphene-sqlalchemy: https://github.com/graphql-python/graphene-sqlalchemy/ The documentation is generated using the excellent
.. _graphene-gae: https://github.com/graphql-python/graphene-gae/ `Sphinx <http://www.sphinx-doc.org/>`__ and a custom theme.
.. _Tracking Issue: https://github.com/graphql-python/graphene/issues/289
.. _examples: examples/ The documentation dependencies are installed by running:
.. _Starwars example: examples/starwars
.. _Starwars Relay example: examples/starwars_relay .. code:: sh
cd docs
pip install -r requirements.txt
Then to produce a HTML version of the documentation:
.. code:: sh
make html
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png .. |Graphene Logo| image:: http://graphene-python.org/favicon.png
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master .. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master

View File

@ -223,3 +223,7 @@ dummy:
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
@echo @echo
@echo "Build finished. Dummy builder generates no files." @echo "Build finished. Dummy builder generates no files."
.PHONY: livehtml
livehtml:
sphinx-autobuild -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html

40
docs/execution/index.rst Normal file
View File

@ -0,0 +1,40 @@
=========
Execution
=========
For executing a query a schema, you can directly call the ``execute`` method on it.
.. code:: python
schema = graphene.Schema(...)
result = schema.execute('{ name }')
``result`` represents he result of execution. ``result.data`` is the result of executing the query, ``result.errors`` is ``None`` if no errors occurred, and is a non-empty list if an error occurred.
Context
_______
You can pass context to a query via ``context_value``.
.. code:: python
class Query(graphene.ObjectType):
name = graphene.String()
def resolve_name(self, args, context, info):
return context.get('name')
schema = graphene.Schema(Query)
result = schema.execute('{ name }', context_value={'name': 'Syrus'})
Middleware
__________
.. toctree::
:maxdepth: 1
middleware

View File

@ -0,0 +1,44 @@
Middleware
==========
You can use ``middleware`` to affect the evaluation of fields in your schema.
A middleware is any object that responds to ``resolve(*args, next_middleware)``.
Inside that method, it should either:
- Send ``resolve`` to the next middleware to continue the evaluation; or
- Return a value to end the evaluation early.
Resolve arguments
-----------------
Middlewares ``resolve`` is invoked with several arguments:
- ``next`` represents the execution chain. Call ``next`` to continue evalution.
- ``root`` is the root value object passed throughout the query.
- ``args`` is the hash of arguments passed to the field.
- ``context`` is the context object passed throughout the query.
- ``info`` is the resolver info.
Example
-------
This middleware only continues evaluation if the ``field_name`` is not ``'user'``
.. code:: python
class AuthorizationMiddleware(object):
def resolve(self, next, root, args, context, info):
if info.field_name == 'user':
return None
return next(root, args, context, info)
And then execute it with:
.. code:: python
result = schema.execute('THE QUERY', middleware=[AuthorizationMiddleware()])

View File

@ -8,6 +8,7 @@ Contents:
quickstart quickstart
types/index types/index
execution/index
relay/index relay/index
Integrations Integrations

View File

@ -1,6 +1,12 @@
Getting started Getting started
=============== ===============
What is GraphQL?
----------------
For an introduction to GraphQL and an overview of its concepts, please refer
to `the official introduction <http://graphql.org/learn/>`.
Lets build a basic GraphQL schema from scratch. Lets build a basic GraphQL schema from scratch.
Requirements Requirements

View File

@ -23,14 +23,14 @@ and ``Droid`` are two implementations of that interface.
name = graphene.String() name = graphene.String()
# Human is a Character implementation # Human is a Character implementation
class Human(ObjectType): class Human(graphene.ObjectType):
class Meta: class Meta:
interfaces = (Character, ) interfaces = (Character, )
born_in = graphene.String() born_in = graphene.String()
# Droid is a Character implementation # Droid is a Character implementation
class Droid(Character): class Droid(graphene.ObjectType):
class Meta: class Meta:
interfaces = (Character, ) interfaces = (Character, )

View File

@ -76,3 +76,69 @@ We should receive:
"ok": true "ok": true
} }
} }
InputFields and InputObjectTypes
----------------------
InputFields are used in mutations to allow nested input data for mutations
To use an InputField you define an InputObjectType that specifies the structure of your input data
.. code:: python
import graphene
class PersonInput(graphene.InputObjectType):
name = graphene.String()
age = graphene.Int()
class CreatePerson(graphene.Mutation):
class Input:
person_data = graphene.InputField(PersonInput)
person = graphene.Field(lambda: Person)
def mutate(self, args, context, info):
p_data = args.get('person_data')
name = p_data.get('name')
age = p_data.get('age')
person = Person(name=name, age=age)
return CreatePerson(person=person)
Note that **name** and **age** are part of **person_data** now
Using the above mutation your new query would look like this:
.. code:: graphql
mutation myFirstMutation {
createPerson(personData: {name:"Peter", age: 24}) {
person {
name,
age
}
}
}
InputObjectTypes can also be fields of InputObjectTypes allowing you to have
as complex of input data as you need
.. code:: python
import graphene
class LatLngInput(graphene.InputObjectType):
lat = graphene.Float()
lng = graphene.Float()
#A location has a latlng associated to it
class LocationInput(graphene.InputObjectType):
name = graphene.String()
latlng = graphene.InputField(LatLngInputType)

View File

@ -9,9 +9,10 @@ Graphene defines the following base Scalar Types:
- ``graphene.Boolean`` - ``graphene.Boolean``
- ``graphene.ID`` - ``graphene.ID``
Graphene also provides custom scalars for Dates and JSON: Graphene also provides custom scalars for Dates, Times, and JSON:
- ``graphene.types.datetime.DateTime`` - ``graphene.types.datetime.DateTime``
- ``graphene.types.datetime.Time``
- ``graphene.types.json.JSONString`` - ``graphene.types.json.JSONString``
@ -24,8 +25,8 @@ The following is an example for creating a DateTime scalar:
.. code:: python .. code:: python
import datetime import datetime
from graphene.core.classtypes import Scalar from graphene.types import Scalar
from graphql.core.language import ast from graphql.language import ast
class DateTime(Scalar): class DateTime(Scalar):
'''DateTime Scalar Description''' '''DateTime Scalar Description'''
@ -57,14 +58,18 @@ Scalars mounted in a ``ObjectType``, ``Interface`` or ``Mutation`` act as
# Is equivalent to: # Is equivalent to:
class Person(graphene.ObjectType): class Person(graphene.ObjectType):
name = graphene.Field(graphene.String()) name = graphene.Field(graphene.String)
**Note:** when using the ``Field`` constructor directly, pass the type and
not an instance.
Types mounted in a ``Field`` act as ``Argument``\ s. Types mounted in a ``Field`` act as ``Argument``\ s.
.. code:: python .. code:: python
graphene.Field(graphene.String(), to=graphene.String()) graphene.Field(graphene.String, to=graphene.String())
# Is equivalent to: # Is equivalent to:
graphene.Field(graphene.String(), to=graphene.Argument(graphene.String())) graphene.Field(graphene.String, to=graphene.Argument(graphene.String()))

View File

@ -0,0 +1,39 @@
import graphene
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
class Query(graphene.ObjectType):
me = graphene.Field(User)
def resolve_me(self, args, context, info):
return context['user']
schema = graphene.Schema(query=Query)
query = '''
query something{
me {
id
name
}
}
'''
def test_query():
result = schema.execute(query, context_value={'user': User(id='1', name='Syrus')})
assert not result.errors
assert result.data == {
'me': {
'id': '1',
'name': 'Syrus',
}
}
if __name__ == '__main__':
result = schema.execute(query, context_value={'user': User(id='X', name='Console')})
print(result.data['me'])

View File

@ -56,7 +56,7 @@ type Ship implements Node {
type ShipConnection { type ShipConnection {
pageInfo: PageInfo! pageInfo: PageInfo!
edges: [ShipEdge] edges: [ShipEdge]!
} }
type ShipEdge { type ShipEdge {

View File

@ -10,7 +10,7 @@ except NameError:
__SETUP__ = False __SETUP__ = False
VERSION = (1, 0, 2, 'final', 0) VERSION = (1, 1, 3, 'final', 0)
__version__ = get_version(VERSION) __version__ = get_version(VERSION)

View File

@ -71,18 +71,18 @@ def _is_descriptor(obj):
def _is_dunder(name): def _is_dunder(name):
"""Returns True if a __dunder__ name, False otherwise.""" """Returns True if a __dunder__ name, False otherwise."""
return (name[:2] == name[-2:] == '__' and return (len(name) > 4 and
name[:2] == name[-2:] == '__' and
name[2:3] != '_' and name[2:3] != '_' and
name[-3:-2] != '_' and name[-3:-2] != '_')
len(name) > 4)
def _is_sunder(name): def _is_sunder(name):
"""Returns True if a _sunder_ name, False otherwise.""" """Returns True if a _sunder_ name, False otherwise."""
return (name[0] == name[-1] == '_' and return (len(name) > 2 and
name[0] == name[-1] == '_' and
name[1:2] != '_' and name[1:2] != '_' and
name[-2:-1] != '_' and name[-2:-1] != '_')
len(name) > 2)
def _make_class_unpicklable(cls): def _make_class_unpicklable(cls):

View File

View File

@ -0,0 +1,41 @@
from ..enum import _is_dunder, _is_sunder
def test__is_dunder():
dunder_names = [
'__i__',
'__test__',
]
non_dunder_names = [
'test',
'__test',
'_test',
'_test_',
'test__',
'',
]
for name in dunder_names:
assert _is_dunder(name) is True
for name in non_dunder_names:
assert _is_dunder(name) is False
def test__is_sunder():
sunder_names = [
'_i_',
'_test_',
]
non_sunder_names = [
'__i__',
'_i__',
'__i_',
'',
]
for name in sunder_names:
assert _is_sunder(name) is True
for name in non_sunder_names:
assert _is_sunder(name) is False

View File

@ -5,6 +5,7 @@ from functools import partial
import six import six
from graphql_relay import connection_from_list from graphql_relay import connection_from_list
from promise import is_thenable, promisify
from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String,
Union) Union)
@ -81,7 +82,7 @@ class ConnectionMeta(ObjectTypeMeta):
class ConnectionBase(AbstractType): class ConnectionBase(AbstractType):
page_info = Field(PageInfo, name='pageInfo', required=True) page_info = Field(PageInfo, name='pageInfo', required=True)
edges = List(edge) edges = NonNull(List(edge))
bases = (ConnectionBase, ) + bases bases = (ConnectionBase, ) + bases
attrs = dict(attrs, _meta=options, Edge=edge) attrs = dict(attrs, _meta=options, Edge=edge)
@ -114,32 +115,41 @@ class IterableConnectionField(Field):
connection_type = type connection_type = type
assert issubclass(connection_type, Connection), ( assert issubclass(connection_type, Connection), (
'{} type have to be a subclass of Connection. Received "{}".' '{} type have to be a subclass of Connection. Received "{}".'
).format(str(self), connection_type) ).format(self.__class__.__name__, connection_type)
return connection_type return connection_type
@classmethod @classmethod
def connection_resolver(cls, resolver, connection, root, args, context, info): def resolve_connection(cls, connection_type, args, resolved):
resolved = resolver(root, args, context, info) if isinstance(resolved, connection_type):
if isinstance(resolved, connection):
return resolved return resolved
assert isinstance(resolved, Iterable), ( assert isinstance(resolved, Iterable), (
'Resolved value from the connection field have to be iterable or instance of {}. ' 'Resolved value from the connection field have to be iterable or instance of {}. '
'Received "{}"' 'Received "{}"'
).format(connection, resolved) ).format(connection_type, resolved)
connection = connection_from_list( connection = connection_from_list(
resolved, resolved,
args, args,
connection_type=connection, connection_type=connection_type,
edge_type=connection.Edge, edge_type=connection_type.Edge,
pageinfo_type=PageInfo pageinfo_type=PageInfo
) )
connection.iterable = resolved connection.iterable = resolved
return connection return connection
@classmethod
def connection_resolver(cls, resolver, connection_type, root, args, context, info):
resolved = resolver(root, args, context, info)
on_resolve = partial(cls.resolve_connection, connection_type, args)
if is_thenable(resolved):
return promisify(resolved).then(on_resolve)
return on_resolve(resolved)
def get_resolver(self, parent_resolver): def get_resolver(self, parent_resolver):
resolver = super(IterableConnectionField, self).get_resolver(parent_resolver) resolver = super(IterableConnectionField, self).get_resolver(parent_resolver)
return partial(self.connection_resolver, resolver, self.type) return partial(self.connection_resolver, resolver, self.type)
ConnectionField = IterableConnectionField ConnectionField = IterableConnectionField

View File

@ -12,9 +12,9 @@ def is_node(objecttype):
''' '''
Check if the given objecttype has Node as an interface Check if the given objecttype has Node as an interface
''' '''
assert issubclass(objecttype, ObjectType), ( if not issubclass(objecttype, ObjectType):
'Only ObjectTypes can have a Node interface.' return False
)
for i in objecttype._meta.interfaces: for i in objecttype._meta.interfaces:
if issubclass(i, Node): if issubclass(i, Node):
return True return True
@ -35,24 +35,26 @@ def get_default_connection(cls):
class GlobalID(Field): class GlobalID(Field):
def __init__(self, node, *args, **kwargs): def __init__(self, node=None, parent_type=None, required=True, *args, **kwargs):
super(GlobalID, self).__init__(ID, *args, **kwargs) super(GlobalID, self).__init__(ID, required=required, *args, **kwargs)
self.node = node self.node = node or Node
self.parent_type_name = parent_type._meta.name if parent_type else None
@staticmethod @staticmethod
def id_resolver(parent_resolver, node, root, args, context, info): def id_resolver(parent_resolver, node, root, args, context, info, parent_type_name=None):
id = parent_resolver(root, args, context, info) type_id = parent_resolver(root, args, context, info)
return node.to_global_id(info.parent_type.name, id) # root._meta.name parent_type_name = parent_type_name or info.parent_type.name
return node.to_global_id(parent_type_name, type_id) # root._meta.name
def get_resolver(self, parent_resolver): def get_resolver(self, parent_resolver):
return partial(self.id_resolver, parent_resolver, self.node) return partial(self.id_resolver, parent_resolver, self.node, parent_type_name=self.parent_type_name)
class NodeMeta(InterfaceMeta): class NodeMeta(InterfaceMeta):
def __new__(cls, name, bases, attrs): def __new__(cls, name, bases, attrs):
cls = InterfaceMeta.__new__(cls, name, bases, attrs) cls = InterfaceMeta.__new__(cls, name, bases, attrs)
cls._meta.fields['id'] = GlobalID(cls, required=True, description='The ID of the object.') cls._meta.fields['id'] = GlobalID(cls, description='The ID of the object.')
return cls return cls

View File

@ -28,8 +28,9 @@ def test_connection():
pageinfo_field = fields['page_info'] pageinfo_field = fields['page_info']
assert isinstance(edge_field, Field) assert isinstance(edge_field, Field)
assert isinstance(edge_field.type, List) assert isinstance(edge_field.type, NonNull)
assert edge_field.type.of_type == MyObjectConnection.Edge assert isinstance(edge_field.type.of_type, List)
assert edge_field.type.of_type.of_type == MyObjectConnection.Edge
assert isinstance(pageinfo_field, Field) assert isinstance(pageinfo_field, Field)
assert isinstance(pageinfo_field.type, NonNull) assert isinstance(pageinfo_field.type, NonNull)

View File

@ -1,6 +1,7 @@
from collections import OrderedDict from collections import OrderedDict
from graphql_relay.utils import base64 from graphql_relay.utils import base64
from promise import Promise
from ...types import ObjectType, Schema, String from ...types import ObjectType, Schema, String
from ..connection import ConnectionField, PageInfo from ..connection import ConnectionField, PageInfo
@ -20,12 +21,16 @@ class Letter(ObjectType):
class Query(ObjectType): class Query(ObjectType):
letters = ConnectionField(Letter) letters = ConnectionField(Letter)
connection_letters = ConnectionField(Letter) connection_letters = ConnectionField(Letter)
promise_letters = ConnectionField(Letter)
node = Node.Field() node = Node.Field()
def resolve_letters(self, args, context, info): def resolve_letters(self, args, context, info):
return list(letters.values()) return list(letters.values())
def resolve_promise_letters(self, args, context, info):
return Promise.resolve(list(letters.values()))
def resolve_connection_letters(self, args, context, info): def resolve_connection_letters(self, args, context, info):
return Letter.Connection( return Letter.Connection(
page_info=PageInfo( page_info=PageInfo(
@ -228,3 +233,38 @@ def test_connection_type_nodes():
} }
} }
} }
def test_connection_promise():
result = schema.execute('''
{
promiseLetters(first:1) {
edges {
node {
id
letter
}
}
pageInfo {
hasPreviousPage
hasNextPage
}
}
}
''')
assert not result.errors
assert result.data == {
'promiseLetters': {
'edges': [{
'node': {
'id': 'TGV0dGVyOjA=',
'letter': 'A',
},
}],
'pageInfo': {
'hasPreviousPage': False,
'hasNextPage': True,
}
}
}

View File

@ -0,0 +1,60 @@
from graphql_relay import to_global_id
from ..node import Node, GlobalID
from ...types import NonNull, ID, ObjectType, String
from ...types.definitions import GrapheneObjectType
class CustomNode(Node):
class Meta:
name = 'Node'
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
class Info(object):
def __init__(self, parent_type):
self.parent_type = GrapheneObjectType(
graphene_type=parent_type,
name=parent_type._meta.name,
description=parent_type._meta.description,
fields=None,
is_type_of=parent_type.is_type_of,
interfaces=None
)
def test_global_id_defaults_to_required_and_node():
gid = GlobalID()
assert isinstance(gid.type, NonNull)
assert gid.type.of_type == ID
assert gid.node == Node
def test_global_id_allows_overriding_of_node_and_required():
gid = GlobalID(node=CustomNode, required=False)
assert gid.type == ID
assert gid.node == CustomNode
def test_global_id_defaults_to_info_parent_type():
my_id = '1'
gid = GlobalID()
id_resolver = gid.get_resolver(lambda *_: my_id)
my_global_id = id_resolver(None, None, None, Info(User))
assert my_global_id == to_global_id(User._meta.name, my_id)
def test_global_id_allows_setting_customer_parent_type():
my_id = '1'
gid = GlobalID(parent_type=User)
id_resolver = gid.get_resolver(lambda *_: my_id)
my_global_id = id_resolver(None, None, None, None)
assert my_global_id == to_global_id(User._meta.name, my_id)

View File

View File

View File

@ -0,0 +1,52 @@
# https://github.com/graphql-python/graphene/issues/313
import graphene
from graphene import resolve_only_args
class Success(graphene.ObjectType):
yeah = graphene.String()
class Error(graphene.ObjectType):
message = graphene.String()
class CreatePostResult(graphene.Union):
class Meta:
types = [Success, Error]
class CreatePost(graphene.Mutation):
class Input:
text = graphene.String(required=True)
result = graphene.Field(CreatePostResult)
@resolve_only_args
def mutate(self, text):
result = Success(yeah='yeah')
return CreatePost(result=result)
class Mutations(graphene.ObjectType):
create_post = CreatePost.Field()
# tests.py
def test_create_post():
query_string = '''
mutation {
createPost(text: "Try this out") {
result {
__typename
}
}
}
'''
schema = graphene.Schema(mutation=Mutations)
result = schema.execute(query_string)
assert not result.errors
assert result.data['createPost']['result']['__typename'] == 'Success'

View File

@ -0,0 +1,24 @@
# https://github.com/graphql-python/graphene/issues/356
import pytest
import graphene
from graphene import relay
class SomeTypeOne(graphene.ObjectType):
pass
class SomeTypeTwo(graphene.ObjectType):
pass
class MyUnion(graphene.Union):
class Meta:
types = (SomeTypeOne, SomeTypeTwo)
def test_issue():
with pytest.raises(Exception) as exc_info:
class Query(graphene.ObjectType):
things = relay.ConnectionField(MyUnion)
schema = graphene.Schema(query=Query)
assert str(exc_info.value) == 'IterableConnectionField type have to be a subclass of Connection. Received "MyUnion".'

View File

@ -1,11 +1,12 @@
from collections import OrderedDict from collections import OrderedDict
from itertools import chain from itertools import chain
from ..utils.orderedtype import OrderedType from .mountedtype import MountedType
from .structures import NonNull from .structures import NonNull
from .dynamic import Dynamic
class Argument(OrderedType): class Argument(MountedType):
def __init__(self, type, default_value=None, description=None, name=None, required=False, _creation_counter=None): def __init__(self, type, default_value=None, description=None, name=None, required=False, _creation_counter=None):
super(Argument, self).__init__(_creation_counter=_creation_counter) super(Argument, self).__init__(_creation_counter=_creation_counter)
@ -27,14 +28,33 @@ class Argument(OrderedType):
) )
def to_arguments(args, extra_args): def to_arguments(args, extra_args=None):
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
extra_args = sorted(extra_args.items(), key=lambda f: f[1]) from .field import Field
from .inputfield import InputField
if extra_args:
extra_args = sorted(extra_args.items(), key=lambda f: f[1])
else:
extra_args = []
iter_arguments = chain(args.items(), extra_args) iter_arguments = chain(args.items(), extra_args)
arguments = OrderedDict() arguments = OrderedDict()
for default_name, arg in iter_arguments: for default_name, arg in iter_arguments:
if isinstance(arg, Dynamic):
arg = arg.get_type()
if arg is None:
# If the Dynamic type returned None
# then we skip the Argument
continue
if isinstance(arg, UnmountedType): if isinstance(arg, UnmountedType):
arg = arg.Argument() arg = Argument.mount(arg)
if isinstance(arg, (InputField, Field)):
raise ValueError('Expected {} to be Argument, but received {}. Try using Argument({}).'.format(
default_name,
type(arg).__name__,
arg.type
))
if not isinstance(arg, Argument): if not isinstance(arg, Argument):
raise ValueError('Unknown argument "{}".'.format(default_name)) raise ValueError('Unknown argument "{}".'.format(default_name))

View File

@ -29,11 +29,37 @@ class DateTime(Scalar):
) )
return dt.isoformat() return dt.isoformat()
@staticmethod @classmethod
def parse_literal(node): def parse_literal(cls, node):
if isinstance(node, ast.StringValue): if isinstance(node, ast.StringValue):
return iso8601.parse_date(node.value) return cls.parse_value(node.value)
@staticmethod @staticmethod
def parse_value(value): def parse_value(value):
return iso8601.parse_date(value) return iso8601.parse_date(value)
class Time(Scalar):
'''
The `Time` scalar type represents a Time value as
specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
'''
epoch_date = '1970-01-01'
@staticmethod
def serialize(time):
assert isinstance(time, datetime.time), (
'Received not compatible time "{}"'.format(repr(time))
)
return time.isoformat()
@classmethod
def parse_literal(cls, node):
if isinstance(node, ast.StringValue):
return cls.parse_value(node.value)
@classmethod
def parse_value(cls, value):
dt = iso8601.parse_date('{}T{}'.format(cls.epoch_date, value))
return datetime.time(dt.hour, dt.minute, dt.second, dt.microsecond, dt.tzinfo)

View File

@ -1,9 +1,9 @@
import inspect import inspect
from ..utils.orderedtype import OrderedType from .mountedtype import MountedType
class Dynamic(OrderedType): class Dynamic(MountedType):
''' '''
A Dynamic Type let us get the type in runtime when we generate A Dynamic Type let us get the type in runtime when we generate
the schema. So we can have lazy fields. the schema. So we can have lazy fields.

View File

@ -58,5 +58,10 @@ class Enum(six.with_metaclass(EnumTypeMeta, UnmountedType)):
kind of type, often integers. kind of type, often integers.
''' '''
def get_type(self): @classmethod
return type(self) def get_type(cls):
'''
This function is called when the unmounted type (Enum instance)
is mounted (as a Field, InputField or Argument)
'''
return cls

View File

@ -2,8 +2,8 @@ import inspect
from collections import Mapping, OrderedDict from collections import Mapping, OrderedDict
from functools import partial from functools import partial
from ..utils.orderedtype import OrderedType
from .argument import Argument, to_arguments from .argument import Argument, to_arguments
from .mountedtype import MountedType
from .structures import NonNull from .structures import NonNull
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
@ -18,7 +18,7 @@ def source_resolver(source, root, args, context, info):
return resolved return resolved
class Field(OrderedType): class Field(MountedType):
def __init__(self, type, args=None, resolver=None, source=None, def __init__(self, type, args=None, resolver=None, source=None,
deprecation_reason=None, name=None, description=None, deprecation_reason=None, name=None, description=None,
@ -60,7 +60,7 @@ class Field(OrderedType):
@property @property
def type(self): def type(self):
if inspect.isfunction(self._type): if inspect.isfunction(self._type) or type(self._type) is partial:
return self._type() return self._type()
return self._type return self._type

View File

@ -1,8 +1,8 @@
from ..utils.orderedtype import OrderedType from .mountedtype import MountedType
from .structures import NonNull from .structures import NonNull
class InputField(OrderedType): class InputField(MountedType):
def __init__(self, type, name=None, default_value=None, def __init__(self, type, name=None, default_value=None,
deprecation_reason=None, description=None, deprecation_reason=None, description=None,

View File

@ -50,4 +50,8 @@ class InputObjectType(six.with_metaclass(InputObjectTypeMeta, UnmountedType)):
@classmethod @classmethod
def get_type(cls): def get_type(cls):
'''
This function is called when the unmounted type (InputObjectType instance)
is mounted (as a Field, InputField or Argument)
'''
return cls return cls

View File

@ -0,0 +1,21 @@
from ..utils.orderedtype import OrderedType
from .unmountedtype import UnmountedType
class MountedType(OrderedType):
@classmethod
def mount(cls, unmounted): # noqa: N802
'''
Mount the UnmountedType instance
'''
assert isinstance(unmounted, UnmountedType), (
'{} can\'t mount {}'
).format(cls.__name__, repr(unmounted))
return cls(
unmounted.get_type(),
*unmounted.args,
_creation_counter=unmounted.creation_counter,
**unmounted.kwargs
)

View File

@ -19,18 +19,18 @@ class Options(object):
for attr_name, value in defaults.items(): for attr_name, value in defaults.items():
if attr_name in meta_attrs: if attr_name in meta_attrs:
value = meta_attrs.pop(attr_name) value = meta_attrs.pop(attr_name)
elif hasattr(meta, attr_name):
value = getattr(meta, attr_name)
setattr(self, attr_name, value) setattr(self, attr_name, value)
# If meta_attrs is not empty, it implicit means # If meta_attrs is not empty, it implicitly means
# it received invalid attributes # it received invalid attributes
if meta_attrs: if meta_attrs:
raise TypeError( raise TypeError(
"Invalid attributes: {}".format( "Invalid attributes: {}".format(
','.join(meta_attrs.keys()) ', '.join(sorted(meta_attrs.keys()))
) )
) )
def __repr__(self): def __repr__(self):
return '<Meta \n{} >'.format(props(self)) options_props = props(self)
props_as_attrs = ' '.join(['{}={}'.format(key, value) for key, value in options_props.items()])
return '<Options {}>'.format(props_as_attrs)

View File

@ -43,8 +43,13 @@ class Scalar(six.with_metaclass(ScalarTypeMeta, UnmountedType)):
@classmethod @classmethod
def get_type(cls): def get_type(cls):
'''
This function is called when the unmounted type (Scalar instance)
is mounted (as a Field, InputField or Argument)
'''
return cls return cls
# As per the GraphQL Spec, Integers are only treated as valid when a valid # As per the GraphQL Spec, Integers are only treated as valid when a valid
# 32-bit signed integer, providing the broadest support across platforms. # 32-bit signed integer, providing the broadest support across platforms.
# #

View File

@ -6,6 +6,7 @@ from graphql.type.introspection import IntrospectionSchema
from graphql.utils.introspection_query import introspection_query from graphql.utils.introspection_query import introspection_query
from graphql.utils.schema_printer import print_schema from graphql.utils.schema_printer import print_schema
from .definitions import GrapheneGraphQLType
from .typemap import TypeMap, is_graphene_type from .typemap import TypeMap, is_graphene_type
@ -46,6 +47,20 @@ class Schema(GraphQLSchema):
def get_subscription_type(self): def get_subscription_type(self):
return self.get_graphql_type(self._subscription) return self.get_graphql_type(self._subscription)
def __getattr__(self, type_name):
'''
This function let the developer select a type in a given schema
by accessing its attrs.
Example: using schema.Query for accessing the "Query" type in the Schema
'''
_type = super(Schema, self).get_type(type_name)
if _type is None:
raise AttributeError('Type "{}" not found in the Schema'.format(type_name))
if isinstance(_type, GrapheneGraphQLType):
return _type.graphene_type
return _type
def get_graphql_type(self, _type): def get_graphql_type(self, _type):
if not _type: if not _type:
return _type return _type
@ -61,9 +76,6 @@ class Schema(GraphQLSchema):
def execute(self, *args, **kwargs): def execute(self, *args, **kwargs):
return graphql(self, *args, **kwargs) return graphql(self, *args, **kwargs)
def register(self, object_type):
self.types.append(object_type)
def introspect(self): def introspect(self):
return self.execute(introspection_query).data return self.execute(introspection_query).data

View File

@ -9,9 +9,22 @@ class Structure(UnmountedType):
def __init__(self, of_type, *args, **kwargs): def __init__(self, of_type, *args, **kwargs):
super(Structure, self).__init__(*args, **kwargs) super(Structure, self).__init__(*args, **kwargs)
if not isinstance(of_type, Structure) and isinstance(of_type, UnmountedType):
cls_name = type(self).__name__
of_type_name = type(of_type).__name__
raise Exception("{} could not have a mounted {}() as inner type. Try with {}({}).".format(
cls_name,
of_type_name,
cls_name,
of_type_name,
))
self.of_type = of_type self.of_type = of_type
def get_type(self): def get_type(self):
'''
This function is called when the unmounted type (List or NonNull instance)
is mounted (as a Field, InputField or Argument)
'''
return self return self
@ -52,7 +65,7 @@ class NonNull(Structure):
super(NonNull, self).__init__(*args, **kwargs) super(NonNull, self).__init__(*args, **kwargs)
assert not isinstance(self.of_type, NonNull), ( assert not isinstance(self.of_type, NonNull), (
'Can only create NonNull of a Nullable GraphQLType but got: {}.' 'Can only create NonNull of a Nullable GraphQLType but got: {}.'
).format(type) ).format(self.of_type)
def __str__(self): def __str__(self):
return '{}!'.format(self.of_type) return '{}!'.format(self.of_type)

View File

@ -1,6 +1,8 @@
import pytest import pytest
from ..argument import Argument from ..argument import Argument, to_arguments
from ..field import Field
from ..inputfield import InputField
from ..structures import NonNull from ..structures import NonNull
from ..scalars import String from ..scalars import String
@ -24,3 +26,38 @@ def test_argument_comparasion():
def test_argument_required(): def test_argument_required():
arg = Argument(String, required=True) arg = Argument(String, required=True)
assert arg.type == NonNull(String) assert arg.type == NonNull(String)
def test_to_arguments():
args = {
'arg_string': Argument(String),
'unmounted_arg': String(required=True)
}
my_args = to_arguments(args)
assert my_args == {
'arg_string': Argument(String),
'unmounted_arg': Argument(String, required=True)
}
def test_to_arguments_raises_if_field():
args = {
'arg_string': Field(String),
}
with pytest.raises(ValueError) as exc_info:
to_arguments(args)
assert str(exc_info.value) == 'Expected arg_string to be Argument, but received Field. Try using Argument(String).'
def test_to_arguments_raises_if_inputfield():
args = {
'arg_string': InputField(String),
}
with pytest.raises(ValueError) as exc_info:
to_arguments(args)
assert str(exc_info.value) == 'Expected arg_string to be Argument, but received InputField. Try using Argument(String).'

View File

@ -1,18 +1,22 @@
import datetime import datetime
import pytz import pytz
from ..datetime import DateTime from ..datetime import DateTime, Time
from ..objecttype import ObjectType from ..objecttype import ObjectType
from ..schema import Schema from ..schema import Schema
class Query(ObjectType): class Query(ObjectType):
datetime = DateTime(_in=DateTime(name='in')) datetime = DateTime(_in=DateTime(name='in'))
time = Time(_at=Time(name='at'))
def resolve_datetime(self, args, context, info): def resolve_datetime(self, args, context, info):
_in = args.get('in') _in = args.get('in')
return _in return _in
def resolve_time(self, args, context, info):
return args.get('at')
schema = Schema(query=Query) schema = Schema(query=Query)
@ -27,6 +31,17 @@ def test_datetime_query():
} }
def test_time_query():
now = datetime.datetime.now().replace(tzinfo=pytz.utc)
time = datetime.time(now.hour, now.minute, now.second, now.microsecond, now.tzinfo)
isoformat = time.isoformat()
result = schema.execute('''{ time(at: "%s") }'''%isoformat)
assert not result.errors
assert result.data == {
'time': isoformat
}
def test_datetime_query_variable(): def test_datetime_query_variable():
now = datetime.datetime.now().replace(tzinfo=pytz.utc) now = datetime.datetime.now().replace(tzinfo=pytz.utc)
isoformat = now.isoformat() isoformat = now.isoformat()
@ -39,3 +54,18 @@ def test_datetime_query_variable():
assert result.data == { assert result.data == {
'datetime': isoformat 'datetime': isoformat
} }
def test_time_query_variable():
now = datetime.datetime.now().replace(tzinfo=pytz.utc)
time = datetime.time(now.hour, now.minute, now.second, now.microsecond, now.tzinfo)
isoformat = time.isoformat()
result = schema.execute(
'''query Test($time: Time){ time(at: $time) }''',
variable_values={'time': isoformat}
)
assert not result.errors
assert result.data == {
'time': isoformat
}

View File

@ -0,0 +1,27 @@
from ..structures import List, NonNull
from ..scalars import String
from ..dynamic import Dynamic
def test_dynamic():
dynamic = Dynamic(lambda: String)
assert dynamic.get_type() == String
assert str(dynamic.get_type()) == 'String'
def test_nonnull():
dynamic = Dynamic(lambda: NonNull(String))
assert dynamic.get_type().of_type == String
assert str(dynamic.get_type()) == 'String!'
def test_list():
dynamic = Dynamic(lambda: List(String))
assert dynamic.get_type().of_type == String
assert str(dynamic.get_type()) == '[String]'
def test_list_non_null():
dynamic = Dynamic(lambda: List(NonNull(String)))
assert dynamic.get_type().of_type.of_type == String
assert str(dynamic.get_type()) == '[String!]'

View File

@ -1,4 +1,7 @@
from ..enum import Enum, PyEnum from ..enum import Enum, PyEnum
from ..field import Field
from ..inputfield import InputField
from ..argument import Argument
def test_enum_construction(): def test_enum_construction():
@ -72,3 +75,39 @@ def test_enum_value_from_class():
assert RGB.RED.value == 1 assert RGB.RED.value == 1
assert RGB.GREEN.value == 2 assert RGB.GREEN.value == 2
assert RGB.BLUE.value == 3 assert RGB.BLUE.value == 3
def test_enum_value_as_unmounted_field():
class RGB(Enum):
RED = 1
GREEN = 2
BLUE = 3
unmounted = RGB()
unmounted_field = unmounted.Field()
assert isinstance(unmounted_field, Field)
assert unmounted_field.type == RGB
def test_enum_value_as_unmounted_inputfield():
class RGB(Enum):
RED = 1
GREEN = 2
BLUE = 3
unmounted = RGB()
unmounted_field = unmounted.InputField()
assert isinstance(unmounted_field, InputField)
assert unmounted_field.type == RGB
def test_enum_value_as_unmounted_argument():
class RGB(Enum):
RED = 1
GREEN = 2
BLUE = 3
unmounted = RGB()
unmounted_field = unmounted.Argument()
assert isinstance(unmounted_field, Argument)
assert unmounted_field.type == RGB

View File

@ -3,6 +3,7 @@ import pytest
from ..argument import Argument from ..argument import Argument
from ..field import Field from ..field import Field
from ..structures import NonNull from ..structures import NonNull
from ..scalars import String
class MyInstance(object): class MyInstance(object):
@ -75,6 +76,20 @@ def test_field_source_func():
assert field.resolver(MyInstance(), {}, None, None) == MyInstance.value_func() assert field.resolver(MyInstance(), {}, None, None) == MyInstance.value_func()
def test_field_source_as_argument():
MyType = object()
field = Field(MyType, source=String())
assert 'source' in field.args
assert field.args['source'].type == String
def test_field_name_as_argument():
MyType = object()
field = Field(MyType, name=String())
assert 'name' in field.args
assert field.args['name'].type == String
def test_field_source_argument_as_kw(): def test_field_source_argument_as_kw():
MyType = object() MyType = object()
field = Field(MyType, b=NonNull(True), c=Argument(None), a=NonNull(False)) field = Field(MyType, b=NonNull(True), c=Argument(None), a=NonNull(False))

View File

@ -1,7 +1,9 @@
from ..abstracttype import AbstractType from ..abstracttype import AbstractType
from ..field import Field from ..field import Field
from ..argument import Argument
from ..inputfield import InputField from ..inputfield import InputField
from ..objecttype import ObjectType
from ..inputobjecttype import InputObjectType from ..inputobjecttype import InputObjectType
from ..unmountedtype import UnmountedType from ..unmountedtype import UnmountedType
@ -61,6 +63,22 @@ def test_generate_inputobjecttype_unmountedtype():
assert isinstance(MyInputObjectType._meta.fields['field'], InputField) assert isinstance(MyInputObjectType._meta.fields['field'], InputField)
def test_generate_inputobjecttype_as_argument():
class MyInputObjectType(InputObjectType):
field = MyScalar()
class MyObjectType(ObjectType):
field = Field(MyType, input=MyInputObjectType())
assert 'field' in MyObjectType._meta.fields
field = MyObjectType._meta.fields['field']
assert isinstance(field, Field)
assert field.type == MyType
assert 'input' in field.args
assert isinstance(field.args['input'], Argument)
assert field.args['input'].type == MyInputObjectType
def test_generate_inputobjecttype_inherit_abstracttype(): def test_generate_inputobjecttype_inherit_abstracttype():
class MyAbstractType(AbstractType): class MyAbstractType(AbstractType):
field1 = MyScalar(MyType) field1 = MyScalar(MyType)

View File

@ -0,0 +1,26 @@
import pytest
from ..mountedtype import MountedType
from ..field import Field
from ..scalars import String
class CustomField(Field):
def __init__(self, *args, **kwargs):
self.metadata = kwargs.pop('metadata', None)
super(CustomField, self).__init__(*args, **kwargs)
def test_mounted_type():
unmounted = String()
mounted = Field.mount(unmounted)
assert isinstance(mounted, Field)
assert mounted.type == String
def test_mounted_type_custom():
unmounted = String(metadata={'hey': 'yo!'})
mounted = CustomField.mount(unmounted)
assert isinstance(mounted, CustomField)
assert mounted.type == String
assert mounted.metadata == {'hey': 'yo!'}

View File

@ -4,6 +4,7 @@ from ..mutation import Mutation
from ..objecttype import ObjectType from ..objecttype import ObjectType
from ..schema import Schema from ..schema import Schema
from ..scalars import String from ..scalars import String
from ..dynamic import Dynamic
def test_generate_mutation_no_args(): def test_generate_mutation_no_args():
@ -47,12 +48,16 @@ def test_mutation_execution():
class CreateUser(Mutation): class CreateUser(Mutation):
class Input: class Input:
name = String() name = String()
dynamic = Dynamic(lambda: String())
dynamic_none = Dynamic(lambda: None)
name = String() name = String()
dynamic = Dynamic(lambda: String())
def mutate(self, args, context, info): def mutate(self, args, context, info):
name = args.get('name') name = args.get('name')
return CreateUser(name=name) dynamic = args.get('dynamic')
return CreateUser(name=name, dynamic=dynamic)
class Query(ObjectType): class Query(ObjectType):
a = String() a = String()
@ -62,14 +67,16 @@ def test_mutation_execution():
schema = Schema(query=Query, mutation=MyMutation) schema = Schema(query=Query, mutation=MyMutation)
result = schema.execute(''' mutation mymutation { result = schema.execute(''' mutation mymutation {
createUser(name:"Peter") { createUser(name:"Peter", dynamic: "dynamic") {
name name
dynamic
} }
} }
''') ''')
assert not result.errors assert not result.errors
assert result.data == { assert result.data == {
'createUser': { 'createUser': {
'name': "Peter" 'name': 'Peter',
'dynamic': 'dynamic',
} }
} }

View File

@ -0,0 +1,30 @@
import pytest
from ..options import Options
def test_options():
class BaseOptions:
option_1 = False
name = True
meta = Options(BaseOptions, name=False, option_1=False)
assert meta.name == True
assert meta.option_1 == False
def test_options_extra_attrs():
class BaseOptions:
name = True
type = True
with pytest.raises(Exception) as exc_info:
meta = Options(BaseOptions)
assert str(exc_info.value) == 'Invalid attributes: name, type'
def test_options_repr():
class BaseOptions:
name = True
meta = Options(BaseOptions, name=False)
assert repr(meta) == '<Options name=True>'

View File

@ -4,12 +4,15 @@ from functools import partial
from graphql import Source, execute, parse, GraphQLError from graphql import Source, execute, parse, GraphQLError
from ..field import Field from ..field import Field
from ..interface import Interface
from ..inputfield import InputField from ..inputfield import InputField
from ..inputobjecttype import InputObjectType from ..inputobjecttype import InputObjectType
from ..objecttype import ObjectType from ..objecttype import ObjectType
from ..scalars import Int, String from ..scalars import Int, String
from ..schema import Schema from ..schema import Schema
from ..structures import List from ..structures import List
from ..union import Union
from ..dynamic import Dynamic
def test_query(): def test_query():
@ -23,6 +26,112 @@ def test_query():
assert executed.data == {'hello': 'World'} assert executed.data == {'hello': 'World'}
def test_query_union():
class one_object(object):
pass
class two_object(object):
pass
class One(ObjectType):
one = String()
@classmethod
def is_type_of(cls, root, context, info):
return isinstance(root, one_object)
class Two(ObjectType):
two = String()
@classmethod
def is_type_of(cls, root, context, info):
return isinstance(root, two_object)
class MyUnion(Union):
class Meta:
types = (One, Two)
class Query(ObjectType):
unions = List(MyUnion)
def resolve_unions(self, args, context, info):
return [one_object(), two_object()]
hello_schema = Schema(Query)
executed = hello_schema.execute('{ unions { __typename } }')
assert not executed.errors
assert executed.data == {
'unions': [{
'__typename': 'One'
}, {
'__typename': 'Two'
}]
}
def test_query_interface():
class one_object(object):
pass
class two_object(object):
pass
class MyInterface(Interface):
base = String()
class One(ObjectType):
class Meta:
interfaces = (MyInterface, )
one = String()
@classmethod
def is_type_of(cls, root, context, info):
return isinstance(root, one_object)
class Two(ObjectType):
class Meta:
interfaces = (MyInterface, )
two = String()
@classmethod
def is_type_of(cls, root, context, info):
return isinstance(root, two_object)
class Query(ObjectType):
interfaces = List(MyInterface)
def resolve_interfaces(self, args, context, info):
return [one_object(), two_object()]
hello_schema = Schema(Query, types=[One, Two])
executed = hello_schema.execute('{ interfaces { __typename } }')
assert not executed.errors
assert executed.data == {
'interfaces': [{
'__typename': 'One'
}, {
'__typename': 'Two'
}]
}
def test_query_dynamic():
class Query(ObjectType):
hello = Dynamic(lambda: String(resolver=lambda *_: 'World'))
hellos = Dynamic(lambda: List(String, resolver=lambda *_: ['Worlds']))
hello_field = Dynamic(lambda: Field(String, resolver=lambda *_: 'Field World'))
hello_schema = Schema(Query)
executed = hello_schema.execute('{ hello hellos helloField }')
assert not executed.errors
assert executed.data == {'hello': 'World', 'hellos': ['Worlds'], 'helloField': 'Field World'}
def test_query_default_value(): def test_query_default_value():
class MyType(ObjectType): class MyType(ObjectType):
field = String() field = String()

View File

@ -0,0 +1,54 @@
import pytest
from ..schema import Schema
from ..objecttype import ObjectType
from ..scalars import String
from ..field import Field
class MyOtherType(ObjectType):
field = String()
class Query(ObjectType):
inner = Field(MyOtherType)
def test_schema():
schema = Schema(Query)
assert schema.get_query_type() == schema.get_graphql_type(Query)
def test_schema_get_type():
schema = Schema(Query)
assert schema.Query == Query
assert schema.MyOtherType == MyOtherType
def test_schema_get_type_error():
schema = Schema(Query)
with pytest.raises(AttributeError) as exc_info:
schema.X
assert str(exc_info.value) == 'Type "X" not found in the Schema'
def test_schema_str():
schema = Schema(Query)
assert str(schema) == """schema {
query: Query
}
type MyOtherType {
field: String
}
type Query {
inner: MyOtherType
}
"""
def test_schema_introspect():
schema = Schema(Query)
assert '__schema' in schema.introspect()

View File

@ -10,12 +10,51 @@ def test_list():
assert str(_list) == '[String]' assert str(_list) == '[String]'
def test_list_with_unmounted_type():
with pytest.raises(Exception) as exc_info:
List(String())
assert str(exc_info.value) == 'List could not have a mounted String() as inner type. Try with List(String).'
def test_list_inherited_works_list():
_list = List(List(String))
assert isinstance(_list.of_type, List)
assert _list.of_type.of_type == String
def test_list_inherited_works_nonnull():
_list = List(NonNull(String))
assert isinstance(_list.of_type, NonNull)
assert _list.of_type.of_type == String
def test_nonnull(): def test_nonnull():
nonnull = NonNull(String) nonnull = NonNull(String)
assert nonnull.of_type == String assert nonnull.of_type == String
assert str(nonnull) == 'String!' assert str(nonnull) == 'String!'
def test_nonnull_inherited_works_list():
_list = NonNull(List(String))
assert isinstance(_list.of_type, List)
assert _list.of_type.of_type == String
def test_nonnull_inherited_dont_work_nonnull():
with pytest.raises(Exception) as exc_info:
NonNull(NonNull(String))
assert str(exc_info.value) == 'Can only create NonNull of a Nullable GraphQLType but got: String!.'
def test_nonnull_with_unmounted_type():
with pytest.raises(Exception) as exc_info:
NonNull(String())
assert str(exc_info.value) == 'NonNull could not have a mounted String() as inner type. Try with NonNull(String).'
def test_list_comparasion(): def test_list_comparasion():
list1 = List(String) list1 = List(String)
list2 = List(String) list2 = List(String)

View File

@ -5,20 +5,26 @@ from functools import partial
from graphql import (GraphQLArgument, GraphQLBoolean, GraphQLField, from graphql import (GraphQLArgument, GraphQLBoolean, GraphQLField,
GraphQLFloat, GraphQLID, GraphQLInputObjectField, GraphQLFloat, GraphQLID, GraphQLInputObjectField,
GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLString) GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLString)
from graphql.type import GraphQLEnumValue
from graphql.execution.executor import get_default_resolve_type_fn from graphql.execution.executor import get_default_resolve_type_fn
from graphql.type import GraphQLEnumValue
from graphql.type.typemap import GraphQLTypeMap from graphql.type.typemap import GraphQLTypeMap
from ..utils.str_converters import to_camel_case
from ..utils.get_unbound_function import get_unbound_function from ..utils.get_unbound_function import get_unbound_function
from ..utils.str_converters import to_camel_case
from .definitions import (GrapheneEnumType, GrapheneInputObjectType,
GrapheneInterfaceType, GrapheneObjectType,
GrapheneScalarType, GrapheneUnionType,
GrapheneGraphQLType)
from .dynamic import Dynamic from .dynamic import Dynamic
from .enum import Enum from .enum import Enum
from .field import Field
from .inputobjecttype import InputObjectType from .inputobjecttype import InputObjectType
from .interface import Interface from .interface import Interface
from .objecttype import ObjectType from .objecttype import ObjectType
from .scalars import ID, Boolean, Float, Int, Scalar, String from .scalars import ID, Boolean, Float, Int, Scalar, String
from .structures import List, NonNull from .structures import List, NonNull
from .union import Union from .union import Union
from .utils import get_field_as
def is_graphene_type(_type): def is_graphene_type(_type):
@ -28,18 +34,18 @@ def is_graphene_type(_type):
return True return True
def resolve_type(resolve_type_func, map, root, context, info): def resolve_type(resolve_type_func, map, type_name, root, context, info):
_type = resolve_type_func(root, context, info) _type = resolve_type_func(root, context, info)
# assert inspect.isclass(_type) and issubclass(_type, ObjectType), (
# 'Received incompatible type "{}".'.format(_type)
# )
if not _type: if not _type:
return get_default_resolve_type_fn(root, context, info, info.return_type) return_type = map[type_name]
return get_default_resolve_type_fn(root, context, info, return_type)
if inspect.isclass(_type) and issubclass(_type, ObjectType): if inspect.isclass(_type) and issubclass(_type, ObjectType):
graphql_type = map.get(_type._meta.name) graphql_type = map.get(_type._meta.name)
assert graphql_type and graphql_type.graphene_type == _type assert graphql_type and graphql_type.graphene_type == _type
return graphql_type return graphql_type
return _type return _type
@ -63,7 +69,7 @@ class TypeMap(GraphQLTypeMap):
return self.reducer(map, type.of_type) return self.reducer(map, type.of_type)
if type._meta.name in map: if type._meta.name in map:
_type = map[type._meta.name] _type = map[type._meta.name]
if is_graphene_type(_type): if isinstance(_type, GrapheneGraphQLType):
assert _type.graphene_type == type assert _type.graphene_type == type
return map return map
if issubclass(type, ObjectType): if issubclass(type, ObjectType):
@ -81,7 +87,6 @@ class TypeMap(GraphQLTypeMap):
return map return map
def construct_scalar(self, map, type): def construct_scalar(self, map, type):
from .definitions import GrapheneScalarType
_scalars = { _scalars = {
String: GraphQLString, String: GraphQLString,
Int: GraphQLInt, Int: GraphQLInt,
@ -104,7 +109,6 @@ class TypeMap(GraphQLTypeMap):
return map return map
def construct_enum(self, map, type): def construct_enum(self, map, type):
from .definitions import GrapheneEnumType
values = OrderedDict() values = OrderedDict()
for name, value in type._meta.enum.__members__.items(): for name, value in type._meta.enum.__members__.items():
values[name] = GraphQLEnumValue( values[name] = GraphQLEnumValue(
@ -122,10 +126,9 @@ class TypeMap(GraphQLTypeMap):
return map return map
def construct_objecttype(self, map, type): def construct_objecttype(self, map, type):
from .definitions import GrapheneObjectType
if type._meta.name in map: if type._meta.name in map:
_type = map[type._meta.name] _type = map[type._meta.name]
if is_graphene_type(_type): if isinstance(_type, GrapheneGraphQLType):
assert _type.graphene_type == type assert _type.graphene_type == type
return map return map
map[type._meta.name] = GrapheneObjectType( map[type._meta.name] = GrapheneObjectType(
@ -146,10 +149,9 @@ class TypeMap(GraphQLTypeMap):
return map return map
def construct_interface(self, map, type): def construct_interface(self, map, type):
from .definitions import GrapheneInterfaceType
_resolve_type = None _resolve_type = None
if type.resolve_type: if type.resolve_type:
_resolve_type = partial(resolve_type, type.resolve_type, map) _resolve_type = partial(resolve_type, type.resolve_type, map, type._meta.name)
map[type._meta.name] = GrapheneInterfaceType( map[type._meta.name] = GrapheneInterfaceType(
graphene_type=type, graphene_type=type,
name=type._meta.name, name=type._meta.name,
@ -162,7 +164,6 @@ class TypeMap(GraphQLTypeMap):
return map return map
def construct_inputobjecttype(self, map, type): def construct_inputobjecttype(self, map, type):
from .definitions import GrapheneInputObjectType
map[type._meta.name] = GrapheneInputObjectType( map[type._meta.name] = GrapheneInputObjectType(
graphene_type=type, graphene_type=type,
name=type._meta.name, name=type._meta.name,
@ -173,10 +174,9 @@ class TypeMap(GraphQLTypeMap):
return map return map
def construct_union(self, map, type): def construct_union(self, map, type):
from .definitions import GrapheneUnionType
_resolve_type = None _resolve_type = None
if type.resolve_type: if type.resolve_type:
_resolve_type = partial(resolve_type, type.resolve_type, map) _resolve_type = partial(resolve_type, type.resolve_type, map, type._meta.name)
types = [] types = []
for i in type._meta.types: for i in type._meta.types:
map = self.construct_objecttype(map, i) map = self.construct_objecttype(map, i)
@ -202,7 +202,7 @@ class TypeMap(GraphQLTypeMap):
fields = OrderedDict() fields = OrderedDict()
for name, field in type._meta.fields.items(): for name, field in type._meta.fields.items():
if isinstance(field, Dynamic): if isinstance(field, Dynamic):
field = field.get_type() field = get_field_as(field.get_type(), _as=Field)
if not field: if not field:
continue continue
map = self.reducer(map, field.type) map = self.reducer(map, field.type)

View File

@ -39,7 +39,11 @@ class Union(six.with_metaclass(UnionMeta)):
to determine which type is actually used when the field is resolved. to determine which type is actually used when the field is resolved.
''' '''
resolve_type = None @classmethod
def resolve_type(cls, instance, context, info):
from .objecttype import ObjectType
if isinstance(instance, ObjectType):
return type(instance)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
raise Exception("An Union cannot be intitialized") raise Exception("A Union cannot be intitialized")

View File

@ -8,7 +8,7 @@ class UnmountedType(OrderedType):
Instead of writing Instead of writing
>>> class MyObjectType(ObjectType): >>> class MyObjectType(ObjectType):
>>> my_field = Field(String(), description='Description here') >>> my_field = Field(String, description='Description here')
It let you write It let you write
>>> class MyObjectType(ObjectType): >>> class MyObjectType(ObjectType):
@ -21,43 +21,35 @@ class UnmountedType(OrderedType):
self.kwargs = kwargs self.kwargs = kwargs
def get_type(self): def get_type(self):
'''
This function is called when the UnmountedType instance
is mounted (as a Field, InputField or Argument)
'''
raise NotImplementedError("get_type not implemented in {}".format(self)) raise NotImplementedError("get_type not implemented in {}".format(self))
def mount_as(self, _as):
return _as.mount(self)
def Field(self): # noqa: N802 def Field(self): # noqa: N802
''' '''
Mount the UnmountedType as Field Mount the UnmountedType as Field
''' '''
from .field import Field from .field import Field
return Field( return self.mount_as(Field)
self.get_type(),
*self.args,
_creation_counter=self.creation_counter,
**self.kwargs
)
def InputField(self): # noqa: N802 def InputField(self): # noqa: N802
''' '''
Mount the UnmountedType as InputField Mount the UnmountedType as InputField
''' '''
from .inputfield import InputField from .inputfield import InputField
return InputField( return self.mount_as(InputField)
self.get_type(),
*self.args,
_creation_counter=self.creation_counter,
**self.kwargs
)
def Argument(self): # noqa: N802 def Argument(self): # noqa: N802
''' '''
Mount the UnmountedType as Argument Mount the UnmountedType as Argument
''' '''
from .argument import Argument from .argument import Argument
return Argument( return self.mount_as(Argument)
self.get_type(),
*self.args,
_creation_counter=self.creation_counter,
**self.kwargs
)
def __eq__(self, other): def __eq__(self, other):
return ( return (

View File

@ -1,8 +1,6 @@
from collections import OrderedDict from collections import OrderedDict
from .dynamic import Dynamic from .mountedtype import MountedType
from .field import Field
from .inputfield import InputField
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
@ -35,34 +33,16 @@ def get_base_fields(bases, _as=None):
return fields return fields
def mount_as(unmounted_field, _as):
'''
Mount the UnmountedType dinamically as Field or InputField
'''
if _as is None:
return unmounted_field
elif _as is Field:
return unmounted_field.Field()
elif _as is InputField:
return unmounted_field.InputField()
raise Exception(
'Unmounted field "{}" cannot be mounted in {}.'.format(
unmounted_field, _as
)
)
def get_field_as(value, _as=None): def get_field_as(value, _as=None):
''' '''
Get type mounted Get type mounted
''' '''
if isinstance(value, (Field, InputField, Dynamic)): if isinstance(value, MountedType):
return value return value
elif isinstance(value, UnmountedType): elif isinstance(value, UnmountedType):
return mount_as(value, _as) if _as is None:
return value
return _as.mount(value)
def yank_fields_from_attrs(attrs, _as=None, delete=True, sort=True): def yank_fields_from_attrs(attrs, _as=None, delete=True, sort=True):

View File

@ -23,3 +23,19 @@ def test_orderedtype_hash():
assert hash(one) == hash(one) assert hash(one) == hash(one)
assert hash(one) != hash(two) assert hash(one) != hash(two)
def test_orderedtype_resetcounter():
one = OrderedType()
two = OrderedType()
one.reset_counter()
assert one > two
def test_orderedtype_non_orderabletypes():
one = OrderedType()
assert one.__lt__(1) == NotImplemented
assert one.__gt__(1) == NotImplemented
assert not one == 1

View File

@ -70,9 +70,9 @@ setup(
install_requires=[ install_requires=[
'six>=1.10.0', 'six>=1.10.0',
'graphql-core>=1.0', 'graphql-core>=1.0.1',
'graphql-relay>=0.4.4', 'graphql-relay>=0.4.5',
'promise', 'promise>=1.0.1',
], ],
tests_require=[ tests_require=[
'pytest>=2.7.2', 'pytest>=2.7.2',

View File

@ -5,12 +5,15 @@ skipsdist = true
[testenv] [testenv]
deps= deps=
pytest>=2.7.2 pytest>=2.7.2
graphql-core>=0.5.1 graphql-core>=1.0.1
graphql-relay>=0.4.3 graphql-relay>=0.4.5
six six
blinker blinker
singledispatch singledispatch
mock mock
pytz
iso8601
pytest-benchmark
setenv = setenv =
PYTHONPATH = .:{envdir} PYTHONPATH = .:{envdir}
commands= commands=