From 8fa6d61271b457bba54d0681e2fa4d1275890714 Mon Sep 17 00:00:00 2001 From: markus Date: Thu, 10 Nov 2016 10:54:03 +0000 Subject: [PATCH 01/14] Added optional parent type to allow usage of GlobalID in mutations. --- graphene/relay/node.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index acaee3e4..ee053442 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -35,17 +35,19 @@ def get_default_connection(cls): class GlobalID(Field): - def __init__(self, node=None, required=True, *args, **kwargs): + def __init__(self, node=None, parent_type=None, required=True, *args, **kwargs): super(GlobalID, self).__init__(ID, required=required, *args, **kwargs) self.node = node or Node + self.parent_type_name = parent_type._meta.name if parent_type else None @staticmethod - def id_resolver(parent_resolver, node, root, args, context, info): - id = parent_resolver(root, args, context, info) - return node.to_global_id(info.parent_type.name, id) # root._meta.name + def id_resolver(parent_resolver, node, root, args, context, info, parent_type_name=None): + type_id = parent_resolver(root, args, context, info) + 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): - 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): From 09969355fa6341d1f6d5b1dc4ac2bfca6a6087d9 Mon Sep 17 00:00:00 2001 From: markus Date: Thu, 10 Nov 2016 11:10:49 +0000 Subject: [PATCH 02/14] Added tests. --- graphene/relay/node.py | 2 +- graphene/relay/tests/test_global_id.py | 42 ++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index ee053442..9d795875 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -47,7 +47,7 @@ class GlobalID(Field): return node.to_global_id(parent_type_name, type_id) # root._meta.name def get_resolver(self, parent_resolver): - return partial(self.id_resolver, parent_resolver, self.node, parent_type_name=self._parent_type_name) + return partial(self.id_resolver, parent_resolver, self.node, parent_type_name=self.parent_type_name) class NodeMeta(InterfaceMeta): diff --git a/graphene/relay/tests/test_global_id.py b/graphene/relay/tests/test_global_id.py index d45b5da6..b0a6c5cb 100644 --- a/graphene/relay/tests/test_global_id.py +++ b/graphene/relay/tests/test_global_id.py @@ -1,6 +1,8 @@ -from ..node import Node, GlobalID +from graphql_relay import to_global_id -from ...types import NonNull, ID +from ..node import Node, GlobalID +from ...types import NonNull, ID, ObjectType, String +from ...types.definitions import GrapheneObjectType class CustomNode(Node): @@ -9,6 +11,26 @@ class CustomNode(Node): 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) @@ -20,3 +42,19 @@ 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) From b1bffc4f8da66a143e43aeb7893293a2911526d3 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 11 Nov 2016 19:40:23 -0800 Subject: [PATCH 03/14] Added context example --- examples/context_example.py | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 examples/context_example.py diff --git a/examples/context_example.py b/examples/context_example.py new file mode 100644 index 00000000..058e578b --- /dev/null +++ b/examples/context_example.py @@ -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']) From b179d012d768aea67ec66a9e3a0b8a89ace490ca Mon Sep 17 00:00:00 2001 From: BossGrand Date: Mon, 14 Nov 2016 13:04:19 -0800 Subject: [PATCH 04/14] Added documentation for InputFields and InputObjectTypes usage --- docs/types/mutations.rst | 66 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index cb2d780b..8de07f32 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -76,3 +76,69 @@ We should receive: "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 + } + ok + } + } + +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) + + From 85cad0efc38997f1a0ea8a20063ad62e2cf4185d Mon Sep 17 00:00:00 2001 From: BossGrand Date: Mon, 14 Nov 2016 17:50:41 -0800 Subject: [PATCH 05/14] fixed typo in documentation --- docs/types/mutations.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index 8de07f32..135eeb16 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -119,9 +119,9 @@ Using the above mutation your new query would look like this: mutation myFirstMutation { createPerson(personData: {name:"Peter", age: 24}) { person { - name + name, + age } - ok } } From d8ab8fec34e82e3de2097da25e6d6189695119f6 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 14 Nov 2016 19:10:32 -0800 Subject: [PATCH 06/14] Fixed lint errors --- graphene/relay/connection.py | 1 + graphene/types/scalars.py | 1 + 2 files changed, 2 insertions(+) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 46dbba98..72f018dc 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -151,4 +151,5 @@ class IterableConnectionField(Field): resolver = super(IterableConnectionField, self).get_resolver(parent_resolver) return partial(self.connection_resolver, resolver, self.type) + ConnectionField = IterableConnectionField diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index 4e6b94b9..6f07c91c 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -49,6 +49,7 @@ class Scalar(six.with_metaclass(ScalarTypeMeta, UnmountedType)): ''' return cls + # As per the GraphQL Spec, Integers are only treated as valid when a valid # 32-bit signed integer, providing the broadest support across platforms. # From 473f97c7b8e60002f5fe10556505db591a514010 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 14 Nov 2016 19:34:10 -0800 Subject: [PATCH 07/14] Improved messaging for Argument transformation. Fixed #364 --- graphene/types/argument.py | 16 +++++++++-- graphene/types/tests/test_argument.py | 39 ++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/graphene/types/argument.py b/graphene/types/argument.py index 043cad2b..8a621b42 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -28,9 +28,14 @@ class Argument(OrderedType): ) -def to_arguments(args, extra_args): +def to_arguments(args, extra_args=None): 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) arguments = OrderedDict() for default_name, arg in iter_arguments: @@ -44,6 +49,13 @@ def to_arguments(args, extra_args): if isinstance(arg, UnmountedType): arg = arg.Argument() + 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): raise ValueError('Unknown argument "{}".'.format(default_name)) diff --git a/graphene/types/tests/test_argument.py b/graphene/types/tests/test_argument.py index 34ed3144..b4cc3d58 100644 --- a/graphene/types/tests/test_argument.py +++ b/graphene/types/tests/test_argument.py @@ -1,6 +1,8 @@ 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 ..scalars import String @@ -24,3 +26,38 @@ def test_argument_comparasion(): def test_argument_required(): arg = Argument(String, required=True) 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).' From 78a1b18e44fddf27ea8cae2c10a10419e4193d0a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 14 Nov 2016 19:38:45 -0800 Subject: [PATCH 08/14] Improved docs for generating documentation. Fixed #353 --- README.md | 18 +++++++++++++ README.rst | 76 +++++++++++++++++++++++++++++++----------------------- 2 files changed, 62 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index c6f924e3..d9a48380 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,21 @@ After developing, the full test suite can be evaluated by running: ```sh 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 +``` diff --git a/README.rst b/README.rst index 72a6a020..39da6b9d 100644 --- a/README.rst +++ b/README.rst @@ -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 `__ to learn how to +upgrade to Graphene ``1.0``. -------------- -|Graphene Logo| `Graphene`_ |Build Status| |PyPI version| |Coverage Status| -=========================================================================== +|Graphene Logo| `Graphene `__ |Build Status| |PyPI version| |Coverage Status| +========================================================================================================= -`Graphene`_ is a Python library for building GraphQL schemas/types fast -and easily. +`Graphene `__ is a Python library for +building GraphQL schemas/types fast and easily. - **Easy to use:** Graphene helps you use GraphQL in Python without effort. - **Relay:** Graphene has builtin support for Relay - **Data agnostic:** Graphene supports any kind of data source: SQL - (Django, SQLAlchemy), NoSQL, custom Python objects, etc. We believe that - by providing a complete API you could plug Graphene anywhere your - data lives and make your data available through GraphQL. + (Django, SQLAlchemy), NoSQL, custom Python objects, etc. We believe + that by providing a complete API you could plug Graphene anywhere + your data lives and make your data available through GraphQL. Integrations ------------ Graphene has multiple integrations with different frameworks: -+---------------------+-------------------------------------+ -| integration | Package | -+=====================+=====================================+ -| Django | `graphene-django`_ | -+---------------------+-------------------------------------+ -| SQLAlchemy | `graphene-sqlalchemy`_ | -+---------------------+-------------------------------------+ -| Google App Engine | `graphene-gae`_ | -+---------------------+-------------------------------------+ -| Peewee | *In progress* (`Tracking Issue`_) | -+---------------------+-------------------------------------+ ++---------------------+----------------------------------------------------------------------------------------------+ +| integration | Package | ++=====================+==============================================================================================+ +| Django | `graphene-django `__ | ++---------------------+----------------------------------------------------------------------------------------------+ +| SQLAlchemy | `graphene-sqlalchemy `__ | ++---------------------+----------------------------------------------------------------------------------------------+ +| Google App Engine | `graphene-gae `__ | ++---------------------+----------------------------------------------------------------------------------------------+ +| Peewee | *In progress* (`Tracking Issue `__) | ++---------------------+----------------------------------------------------------------------------------------------+ Installation ------------ @@ -45,7 +46,8 @@ For instaling graphene, just run this command in your shell 1.0 Upgrade Guide ----------------- -Please read `UPGRADE-v1.0.md`_ to learn how to upgrade. +Please read `UPGRADE-v1.0.md `__ to learn how to +upgrade. Examples -------- @@ -74,10 +76,11 @@ Then Querying ``graphene.Schema`` is as simple as: result = schema.execute(query) If you want to learn even more, you can also check the following -`examples`_: +`examples `__: -- **Basic Schema**: `Starwars example`_ -- **Relay Schema**: `Starwars Relay example`_ +- **Basic Schema**: `Starwars example `__ +- **Relay Schema**: `Starwars Relay + example `__ 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 -.. _UPGRADE-v1.0.md: /UPGRADE-v1.0.md -.. _Graphene: http://graphene-python.org -.. _graphene-django: https://github.com/graphql-python/graphene-django/ -.. _graphene-sqlalchemy: https://github.com/graphql-python/graphene-sqlalchemy/ -.. _graphene-gae: https://github.com/graphql-python/graphene-gae/ -.. _Tracking Issue: https://github.com/graphql-python/graphene/issues/289 -.. _examples: examples/ -.. _Starwars example: examples/starwars -.. _Starwars Relay example: examples/starwars_relay +Documentation +~~~~~~~~~~~~~ + +The documentation is generated using the excellent +`Sphinx `__ and a custom theme. + +The documentation dependencies are installed by running: + +.. 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 .. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master From 2e58f53f18050230f5947159334edf2528f78dca Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 14 Nov 2016 19:52:40 -0800 Subject: [PATCH 09/14] Improved List/NonNull of_type exceptions and tests. Fixed #337 --- graphene/types/structures.py | 11 ++++++- graphene/types/tests/test_structures.py | 39 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/graphene/types/structures.py b/graphene/types/structures.py index 1346155d..1ecfa83d 100644 --- a/graphene/types/structures.py +++ b/graphene/types/structures.py @@ -9,6 +9,15 @@ class Structure(UnmountedType): def __init__(self, of_type, *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 def get_type(self): @@ -56,7 +65,7 @@ class NonNull(Structure): super(NonNull, self).__init__(*args, **kwargs) assert not isinstance(self.of_type, NonNull), ( 'Can only create NonNull of a Nullable GraphQLType but got: {}.' - ).format(type) + ).format(self.of_type) def __str__(self): return '{}!'.format(self.of_type) diff --git a/graphene/types/tests/test_structures.py b/graphene/types/tests/test_structures.py index 9027895e..e45f09e2 100644 --- a/graphene/types/tests/test_structures.py +++ b/graphene/types/tests/test_structures.py @@ -10,12 +10,51 @@ def test_list(): 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(): nonnull = NonNull(String) assert nonnull.of_type == 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(): list1 = List(String) list2 = List(String) From 0efee6be5d123453756d42bf9ded80becae0992d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 14 Nov 2016 20:14:21 -0800 Subject: [PATCH 10/14] Fixed Union resolve_type. Fixed #313 --- graphene/tests/__init__.py | 0 graphene/tests/issues/__init__.py | 0 graphene/tests/issues/test_313.py | 52 +++++++++++++++++++++++++++++++ graphene/types/union.py | 6 +++- 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 graphene/tests/__init__.py create mode 100644 graphene/tests/issues/__init__.py create mode 100644 graphene/tests/issues/test_313.py diff --git a/graphene/tests/__init__.py b/graphene/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/tests/issues/__init__.py b/graphene/tests/issues/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/tests/issues/test_313.py b/graphene/tests/issues/test_313.py new file mode 100644 index 00000000..bed87290 --- /dev/null +++ b/graphene/tests/issues/test_313.py @@ -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' \ No newline at end of file diff --git a/graphene/types/union.py b/graphene/types/union.py index 622f465e..3d236000 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -39,7 +39,11 @@ class Union(six.with_metaclass(UnionMeta)): 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): raise Exception("A Union cannot be intitialized") From bb7976a75f2a86313b718f62db6ed624d00bf02d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 14 Nov 2016 20:27:06 -0800 Subject: [PATCH 11/14] Improved ConnectionField exception message. Fixed #356 --- graphene/relay/connection.py | 2 +- graphene/relay/node.py | 6 +++--- graphene/tests/issues/test_356.py | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 graphene/tests/issues/test_356.py diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 72f018dc..339e4266 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -115,7 +115,7 @@ class IterableConnectionField(Field): connection_type = type assert issubclass(connection_type, Connection), ( '{} type have to be a subclass of Connection. Received "{}".' - ).format(str(self), connection_type) + ).format(self.__class__.__name__, connection_type) return connection_type @classmethod diff --git a/graphene/relay/node.py b/graphene/relay/node.py index 9d795875..3db30e93 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -12,9 +12,9 @@ def is_node(objecttype): ''' Check if the given objecttype has Node as an interface ''' - assert issubclass(objecttype, ObjectType), ( - 'Only ObjectTypes can have a Node interface. Received %s' - ) % objecttype + if not issubclass(objecttype, ObjectType): + return False + for i in objecttype._meta.interfaces: if issubclass(i, Node): return True diff --git a/graphene/tests/issues/test_356.py b/graphene/tests/issues/test_356.py new file mode 100644 index 00000000..605594e1 --- /dev/null +++ b/graphene/tests/issues/test_356.py @@ -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".' From cc776e8def58b0130e161c1bf244b39c5acab89a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 14 Nov 2016 20:49:13 -0800 Subject: [PATCH 12/14] Added execution context example. Fixed #306 --- docs/Makefile | 4 ++++ docs/execution/index.rst | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/docs/Makefile b/docs/Makefile index 7da67c31..2973acec 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -223,3 +223,7 @@ dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." + +.PHONY: livehtml +livehtml: + sphinx-autobuild -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/docs/execution/index.rst b/docs/execution/index.rst index beb4e3b1..849832d4 100644 --- a/docs/execution/index.rst +++ b/docs/execution/index.rst @@ -2,6 +2,38 @@ 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 From 90e8e30a3a343e57789a4f2695ccf7eab1e3a29f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 14 Nov 2016 21:29:24 -0800 Subject: [PATCH 13/14] Added PyPI upload based on travis --- .travis.yml | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index b313bff4..541c0c0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,19 +6,19 @@ python: - 3.5 - pypy before_install: - - | - if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then - export PYENV_ROOT="$HOME/.pyenv" - if [ -f "$PYENV_ROOT/bin/pyenv" ]; then - cd "$PYENV_ROOT" && git pull - else - rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" - fi - export PYPY_VERSION="4.0.1" - "$PYENV_ROOT/bin/pyenv" install "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" - fi +- | + if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then + export PYENV_ROOT="$HOME/.pyenv" + if [ -f "$PYENV_ROOT/bin/pyenv" ]; then + cd "$PYENV_ROOT" && git pull + else + rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" + fi + export PYPY_VERSION="4.0.1" + "$PYENV_ROOT/bin/pyenv" install "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" + fi install: - | if [ "$TEST_TYPE" = build ]; then @@ -52,3 +52,10 @@ matrix: include: - python: '2.7' 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= From 3c99302ed6edc0140e98d21838c161d6055d11cf Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 14 Nov 2016 22:19:38 -0800 Subject: [PATCH 14/14] Updated version to 1.1.0 --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index 7a01ed16..aa110279 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -10,7 +10,7 @@ except NameError: __SETUP__ = False -VERSION = (1, 0, 2, 'final', 0) +VERSION = (1, 1, 0, 'final', 0) __version__ = get_version(VERSION)