From ef18eb5ce3ed8c65a1cf57c139cd5380f76ef707 Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Thu, 13 Oct 2016 14:08:28 +0100 Subject: [PATCH 01/60] Improve `is_node` error message --- graphene/relay/node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index 85d69276..c1bbe6d5 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -13,8 +13,8 @@ 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.' - ) + 'Only ObjectTypes can have a Node interface. Received %s' + ) % objecttype for i in objecttype._meta.interfaces: if issubclass(i, Node): return True From 822b030938533d4b2b182bd6bf64172f8816c0f4 Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Thu, 13 Oct 2016 14:47:43 +0100 Subject: [PATCH 02/60] Added tests for dynamic field and make more consistent. --- graphene/types/tests/test_dynamic.py | 27 +++++++++++++++++++++++++++ graphene/types/tests/test_query.py | 14 ++++++++++++++ graphene/types/typemap.py | 4 +++- 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 graphene/types/tests/test_dynamic.py diff --git a/graphene/types/tests/test_dynamic.py b/graphene/types/tests/test_dynamic.py new file mode 100644 index 00000000..61dcbd81 --- /dev/null +++ b/graphene/types/tests/test_dynamic.py @@ -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!]' diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index 4f9d8810..b1c5d112 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -10,6 +10,7 @@ from ..objecttype import ObjectType from ..scalars import Int, String from ..schema import Schema from ..structures import List +from ..dynamic import Dynamic def test_query(): @@ -23,6 +24,19 @@ def test_query(): assert executed.data == {'hello': 'World'} +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(): class MyType(ObjectType): field = String() diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index c2b47279..d7a7f23e 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -13,12 +13,14 @@ from ..utils.str_converters import to_camel_case from ..utils.get_unbound_function import get_unbound_function from .dynamic import Dynamic from .enum import Enum +from .field import Field from .inputobjecttype import InputObjectType from .interface import Interface from .objecttype import ObjectType from .scalars import ID, Boolean, Float, Int, Scalar, String from .structures import List, NonNull from .union import Union +from .utils import get_field_as def is_graphene_type(_type): @@ -202,7 +204,7 @@ class TypeMap(GraphQLTypeMap): fields = OrderedDict() for name, field in type._meta.fields.items(): if isinstance(field, Dynamic): - field = field.get_type() + field = get_field_as(field.get_type(), _as=Field) if not field: continue map = self.reducer(map, field.type) From 04085911410f247a918b017d2d4d62910f4dbe0e Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Thu, 13 Oct 2016 16:54:54 +0100 Subject: [PATCH 03/60] Added additional tests. --- graphene/types/argument.py | 4 ++++ graphene/types/tests/test_mutation.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/graphene/types/argument.py b/graphene/types/argument.py index 49784b10..ead2c005 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -3,6 +3,7 @@ from itertools import chain from ..utils.orderedtype import OrderedType from .structures import NonNull +from .dynamic import Dynamic class Argument(OrderedType): @@ -33,6 +34,9 @@ def to_arguments(args, extra_args): iter_arguments = chain(args.items(), extra_args) arguments = OrderedDict() for default_name, arg in iter_arguments: + if isinstance(arg, Dynamic): + arg = arg.get_type() + if isinstance(arg, UnmountedType): arg = arg.Argument() diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 2af6f4fd..24096161 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -4,6 +4,7 @@ from ..mutation import Mutation from ..objecttype import ObjectType from ..schema import Schema from ..scalars import String +from ..dynamic import Dynamic def test_generate_mutation_no_args(): @@ -47,12 +48,15 @@ def test_mutation_execution(): class CreateUser(Mutation): class Input: name = String() + dynamic = Dynamic(lambda: String()) name = String() + dynamic = Dynamic(lambda: String()) def mutate(self, args, context, info): name = args.get('name') - return CreateUser(name=name) + dynamic = args.get('dynamic') + return CreateUser(name=name, dynamic=dynamic) class Query(ObjectType): a = String() @@ -62,14 +66,16 @@ def test_mutation_execution(): schema = Schema(query=Query, mutation=MyMutation) result = schema.execute(''' mutation mymutation { - createUser(name:"Peter") { + createUser(name:"Peter", dynamic: "dynamic") { name + dynamic } } ''') assert not result.errors assert result.data == { 'createUser': { - 'name': "Peter" + 'name': 'Peter', + 'dynamic': 'dynamic', } } From 77e4ead0d76de2e7d0e99b4553140ad02b21b9b8 Mon Sep 17 00:00:00 2001 From: Wenley Tong Date: Sun, 16 Oct 2016 11:30:51 -0700 Subject: [PATCH 04/60] Tweak the documentation to match behavior in the playground. --- docs/types/scalars.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index d8e22b54..139750ee 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -1,7 +1,7 @@ Scalars ======= -Graphene define the following base Scalar Types: +Graphene defines the following base Scalar Types: - ``graphene.String`` - ``graphene.Int`` @@ -47,8 +47,8 @@ The following is an example for creating a DateTime scalar: Mounting Scalars ---------------- -This scalars if are mounted in a ``ObjectType``, ``Interface`` or -``Mutation``, would act as ``Field``\ s. +These scalars, if are mounted in a ``ObjectType``, ``Interface`` or +``Mutation``, act as ``Field``\ s. Note: when using the ``Field`` constructor directly, pass the type and not an instance. .. code:: python @@ -57,14 +57,14 @@ This scalars if are mounted in a ``ObjectType``, ``Interface`` or # Is equivalent to: class Person(graphene.ObjectType): - name = graphene.Field(graphene.String()) + name = graphene.Field(graphene.String) If the types are mounted in a ``Field``, would act as ``Argument``\ s. .. code:: python - graphene.Field(graphene.String(), to=graphene.String()) + graphene.Field(graphene.String, to=graphene.String()) # Is equivalent to: - graphene.Field(graphene.String(), to=graphene.Argument(graphene.String())) + graphene.Field(graphene.String, to=graphene.Argument(graphene.String())) From 0a80119f5eabbee2e34d0aa755afb9847ae2cf2c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 16 Oct 2016 13:14:08 -0700 Subject: [PATCH 05/60] Fixed Dynamic arguments --- graphene/types/argument.py | 4 ++++ graphene/types/tests/test_mutation.py | 1 + 2 files changed, 5 insertions(+) diff --git a/graphene/types/argument.py b/graphene/types/argument.py index ead2c005..043cad2b 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -36,6 +36,10 @@ def to_arguments(args, extra_args): 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): arg = arg.Argument() diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 24096161..8ff8773f 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -49,6 +49,7 @@ def test_mutation_execution(): class Input: name = String() dynamic = Dynamic(lambda: String()) + dynamic_none = Dynamic(lambda: None) name = String() dynamic = Dynamic(lambda: String()) From 47d7adf7b0f64efb9f687dd4a9574b4be39ba764 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 21 Oct 2016 08:44:44 -0700 Subject: [PATCH 06/60] Improved documentation in UnmountedTypes --- graphene/types/enum.py | 9 +++++++-- graphene/types/inputobjecttype.py | 4 ++++ graphene/types/scalars.py | 4 ++++ graphene/types/structures.py | 4 ++++ graphene/types/unmountedtype.py | 6 +++++- 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 7895f258..3bff137c 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -58,5 +58,10 @@ class Enum(six.with_metaclass(EnumTypeMeta, UnmountedType)): kind of type, often integers. ''' - def get_type(self): - return type(self) + @classmethod + def get_type(cls): + ''' + This function is called when the unmounted type (Enum instance) + is mounted (as a Field, InputField or Argument) + ''' + return cls diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 2dc91cd5..cbc13f95 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -50,4 +50,8 @@ class InputObjectType(six.with_metaclass(InputObjectTypeMeta, UnmountedType)): @classmethod def get_type(cls): + ''' + This function is called when the unmounted type (InputObjectType instance) + is mounted (as a Field, InputField or Argument) + ''' return cls diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index d6060d33..4e6b94b9 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -43,6 +43,10 @@ class Scalar(six.with_metaclass(ScalarTypeMeta, UnmountedType)): @classmethod def get_type(cls): + ''' + This function is called when the unmounted type (Scalar instance) + is mounted (as a Field, InputField or Argument) + ''' return cls # As per the GraphQL Spec, Integers are only treated as valid when a valid diff --git a/graphene/types/structures.py b/graphene/types/structures.py index 6c9c0e7e..1346155d 100644 --- a/graphene/types/structures.py +++ b/graphene/types/structures.py @@ -12,6 +12,10 @@ class Structure(UnmountedType): self.of_type = of_type 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 diff --git a/graphene/types/unmountedtype.py b/graphene/types/unmountedtype.py index c9b36631..e910421b 100644 --- a/graphene/types/unmountedtype.py +++ b/graphene/types/unmountedtype.py @@ -8,7 +8,7 @@ class UnmountedType(OrderedType): Instead of writing >>> class MyObjectType(ObjectType): - >>> my_field = Field(String(), description='Description here') + >>> my_field = Field(String, description='Description here') It let you write >>> class MyObjectType(ObjectType): @@ -21,6 +21,10 @@ class UnmountedType(OrderedType): self.kwargs = kwargs 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)) def Field(self): # noqa: N802 From 24b85d318ce09fbc9730852a35d439112e764301 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 21 Oct 2016 09:00:33 -0700 Subject: [PATCH 07/60] Improved OrderedType coverage --- graphene/utils/tests/test_orderedtype.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/graphene/utils/tests/test_orderedtype.py b/graphene/utils/tests/test_orderedtype.py index 2845b876..ea6c7cc0 100644 --- a/graphene/utils/tests/test_orderedtype.py +++ b/graphene/utils/tests/test_orderedtype.py @@ -23,3 +23,19 @@ def test_orderedtype_hash(): assert hash(one) == hash(one) 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 From e1e24327b07e377125cb813f0f80f290d02c641f Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 21 Oct 2016 09:12:28 -0700 Subject: [PATCH 08/60] Improved options testing and coverage --- graphene/types/options.py | 10 +++++----- graphene/types/tests/test_options.py | 30 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 graphene/types/tests/test_options.py diff --git a/graphene/types/options.py b/graphene/types/options.py index 50e982c7..0002db68 100644 --- a/graphene/types/options.py +++ b/graphene/types/options.py @@ -19,18 +19,18 @@ class Options(object): for attr_name, value in defaults.items(): if attr_name in meta_attrs: value = meta_attrs.pop(attr_name) - elif hasattr(meta, attr_name): - value = getattr(meta, attr_name) 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 if meta_attrs: raise TypeError( "Invalid attributes: {}".format( - ','.join(meta_attrs.keys()) + ', '.join(sorted(meta_attrs.keys())) ) ) def __repr__(self): - return ''.format(props(self)) + options_props = props(self) + props_as_attrs = ' '.join(['{}={}'.format(key, value) for key, value in options_props.items()]) + return ''.format(props_as_attrs) diff --git a/graphene/types/tests/test_options.py b/graphene/types/tests/test_options.py new file mode 100644 index 00000000..fbcba2db --- /dev/null +++ b/graphene/types/tests/test_options.py @@ -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) == '' From d7fded7d983635e8f54695502cd838d11ec29c6d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 21 Oct 2016 09:19:13 -0700 Subject: [PATCH 09/60] Improved Enum coverage --- graphene/types/tests/test_enum.py | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 9160df8c..a5a2d4c1 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -1,4 +1,7 @@ from ..enum import Enum, PyEnum +from ..field import Field +from ..inputfield import InputField +from ..argument import Argument def test_enum_construction(): @@ -72,3 +75,39 @@ def test_enum_value_from_class(): assert RGB.RED.value == 1 assert RGB.GREEN.value == 2 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 From ecdfed257cb6ea7ae3191bf5b5976420edb99787 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 21 Oct 2016 09:22:19 -0700 Subject: [PATCH 10/60] Improved Field coverage --- graphene/types/tests/test_field.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/graphene/types/tests/test_field.py b/graphene/types/tests/test_field.py index 7ca557ba..7633e251 100644 --- a/graphene/types/tests/test_field.py +++ b/graphene/types/tests/test_field.py @@ -3,6 +3,7 @@ import pytest from ..argument import Argument from ..field import Field from ..structures import NonNull +from ..scalars import String class MyInstance(object): @@ -75,6 +76,20 @@ def test_field_source_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(): MyType = object() field = Field(MyType, b=NonNull(True), c=Argument(None), a=NonNull(False)) From ea69be61174a7afcc3d61f36f30422fe75cbc687 Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Wed, 26 Oct 2016 10:50:27 +0100 Subject: [PATCH 11/60] Add support to return a promise for connections. --- graphene/relay/connection.py | 37 +++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index e63478e5..05f4a68a 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -118,25 +118,28 @@ class IterableConnectionField(Field): return connection_type @classmethod - def connection_resolver(cls, resolver, connection, root, args, context, info): - resolved = resolver(root, args, context, info) + def connection_resolver(cls, resolver, connection_type, root, args, context, info): + p = Promise.resolve(resolver(root, args, context, info)) - if isinstance(resolved, connection): - return resolved + def resolve_connection(resolved): + if isinstance(resolved, connection_type): + return resolved - assert isinstance(resolved, Iterable), ( - 'Resolved value from the connection field have to be iterable or instance of {}. ' - 'Received "{}"' - ).format(connection, resolved) - connection = connection_from_list( - resolved, - args, - connection_type=connection, - edge_type=connection.Edge, - pageinfo_type=PageInfo - ) - connection.iterable = resolved - return connection + assert isinstance(resolved, Iterable), ( + 'Resolved value from the connection field have to be iterable or instance of {}. ' + 'Received "{}"' + ).format(connection_type, resolved) + connection = connection_from_list( + resolved, + args, + connection_type=connection_type, + edge_type=connection_type.Edge, + pageinfo_type=PageInfo + ) + connection.iterable = resolved + return connection + + return p.then(resolve_connection) def get_resolver(self, parent_resolver): resolver = super(IterableConnectionField, self).get_resolver(parent_resolver) From 16e9f221b52510bd1ac54ff5691e4f2a506dda4b Mon Sep 17 00:00:00 2001 From: Markus Padourek Date: Wed, 26 Oct 2016 10:52:46 +0100 Subject: [PATCH 12/60] added missing import --- graphene/relay/connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 05f4a68a..b8f49207 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -5,6 +5,7 @@ from functools import partial import six from graphql_relay import connection_from_list +from promise import Promise from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union) From 760ccc83587396df9a2cbf5806b7f4fa73ba620e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 27 Oct 2016 02:41:36 +0200 Subject: [PATCH 13/60] Improved Promise connection abstraction --- graphene/relay/connection.py | 43 ++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index b8f49207..20f3cceb 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -118,29 +118,34 @@ class IterableConnectionField(Field): ).format(str(self), connection_type) return connection_type + @classmethod + def resolve_connection(cls, connection_type, args, resolved): + if isinstance(resolved, connection_type): + return resolved + + assert isinstance(resolved, Iterable), ( + 'Resolved value from the connection field have to be iterable or instance of {}. ' + 'Received "{}"' + ).format(connection_type, resolved) + connection = connection_from_list( + resolved, + args, + connection_type=connection_type, + edge_type=connection_type.Edge, + pageinfo_type=PageInfo + ) + connection.iterable = resolved + return connection + @classmethod def connection_resolver(cls, resolver, connection_type, root, args, context, info): - p = Promise.resolve(resolver(root, args, context, info)) + resolved = resolver(root, args, context, info) - def resolve_connection(resolved): - if isinstance(resolved, connection_type): - return resolved + on_resolve = partial(cls.resolve_connection, connection_type, args) + if isinstance(resolved, Promise): + return resolved.then(on_resolve) - assert isinstance(resolved, Iterable), ( - 'Resolved value from the connection field have to be iterable or instance of {}. ' - 'Received "{}"' - ).format(connection_type, resolved) - connection = connection_from_list( - resolved, - args, - connection_type=connection_type, - edge_type=connection_type.Edge, - pageinfo_type=PageInfo - ) - connection.iterable = resolved - return connection - - return p.then(resolve_connection) + return on_resolve(resolved) def get_resolver(self, parent_resolver): resolver = super(IterableConnectionField, self).get_resolver(parent_resolver) From c7a48c3c2c09bfe2cf434998a55d5051d8a001bf Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 27 Oct 2016 02:48:36 +0200 Subject: [PATCH 14/60] Added connection promise tests --- graphene/relay/tests/test_connection_query.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index cc1f12ce..068081d6 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -1,6 +1,7 @@ from collections import OrderedDict from graphql_relay.utils import base64 +from promise import Promise from ...types import ObjectType, Schema, String from ..connection import ConnectionField, PageInfo @@ -20,12 +21,16 @@ class Letter(ObjectType): class Query(ObjectType): letters = ConnectionField(Letter) connection_letters = ConnectionField(Letter) + promise_letters = ConnectionField(Letter) node = Node.Field() def resolve_letters(self, args, context, info): 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): return Letter.Connection( 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, + } + } + } From 495361ef53ffc3edb3ffa0966cdcb0f67618056c Mon Sep 17 00:00:00 2001 From: Eran Kampf Date: Thu, 27 Oct 2016 10:27:13 -0700 Subject: [PATCH 15/60] Edges should be NonNull --- graphene/relay/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 20f3cceb..46dbba98 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -82,7 +82,7 @@ class ConnectionMeta(ObjectTypeMeta): class ConnectionBase(AbstractType): page_info = Field(PageInfo, name='pageInfo', required=True) - edges = List(edge) + edges = NonNull(List(edge)) bases = (ConnectionBase, ) + bases attrs = dict(attrs, _meta=options, Edge=edge) From 344d85c19e3be0dc09de228a25eaa34b666346f5 Mon Sep 17 00:00:00 2001 From: Eran Kampf Date: Thu, 27 Oct 2016 10:35:31 -0700 Subject: [PATCH 16/60] fix tests --- examples/starwars_relay/tests/test_objectidentification.py | 2 +- graphene/relay/tests/test_connection.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/starwars_relay/tests/test_objectidentification.py b/examples/starwars_relay/tests/test_objectidentification.py index 87394421..327d5b0c 100644 --- a/examples/starwars_relay/tests/test_objectidentification.py +++ b/examples/starwars_relay/tests/test_objectidentification.py @@ -56,7 +56,7 @@ type Ship implements Node { type ShipConnection { pageInfo: PageInfo! - edges: [ShipEdge] + edges: [ShipEdge]! } type ShipEdge { diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index 18d890c1..87f937ae 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -28,8 +28,9 @@ def test_connection(): pageinfo_field = fields['page_info'] assert isinstance(edge_field, Field) - assert isinstance(edge_field.type, List) - assert edge_field.type.of_type == MyObjectConnection.Edge + assert isinstance(edge_field.type, NonNull) + 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.type, NonNull) From adfbffb267b8e4c7bf6067c743a22a1e3c990d90 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 31 Oct 2016 05:32:00 +0100 Subject: [PATCH 17/60] Added middleware docs --- docs/execution/index.rst | 8 +++++++ docs/execution/middleware.rst | 44 +++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 3 files changed, 53 insertions(+) create mode 100644 docs/execution/index.rst create mode 100644 docs/execution/middleware.rst diff --git a/docs/execution/index.rst b/docs/execution/index.rst new file mode 100644 index 00000000..beb4e3b1 --- /dev/null +++ b/docs/execution/index.rst @@ -0,0 +1,8 @@ +========= +Execution +========= + +.. toctree:: + :maxdepth: 1 + + middleware diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst new file mode 100644 index 00000000..54eb55db --- /dev/null +++ b/docs/execution/middleware.rst @@ -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()]) diff --git a/docs/index.rst b/docs/index.rst index d7aa9dca..675051b3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,6 +8,7 @@ Contents: quickstart types/index + execution/index relay/index Integrations From f5c5d33639b6b9f089aa527de65b46a28d8adba0 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Thu, 3 Nov 2016 22:08:41 +0100 Subject: [PATCH 18/60] Fix import paths to match graphene>=1.0 paths --- docs/types/scalars.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index 139750ee..4cfc09fd 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -24,8 +24,8 @@ The following is an example for creating a DateTime scalar: .. code:: python import datetime - from graphene.core.classtypes import Scalar - from graphql.core.language import ast + from graphene.types import Scalar + from graphql.language import ast class DateTime(Scalar): '''DateTime Scalar Description''' From b2d7dfe5461913f94b5859f071d5b998a5b22afc Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Thu, 3 Nov 2016 22:17:37 +0100 Subject: [PATCH 19/60] Fix sentences to be more readable --- docs/types/scalars.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index 4cfc09fd..a75c70e4 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -47,8 +47,8 @@ The following is an example for creating a DateTime scalar: Mounting Scalars ---------------- -These scalars, if are mounted in a ``ObjectType``, ``Interface`` or -``Mutation``, act as ``Field``\ s. Note: when using the ``Field`` constructor directly, pass the type and not an instance. +If a scalar is mounted in an ``ObjectType``, ``Interface`` or +``Mutation``, they act as ``Field``\ s: .. code:: python @@ -60,7 +60,11 @@ These scalars, if are mounted in a ``ObjectType``, ``Interface`` or name = graphene.Field(graphene.String) -If the types are mounted in a ``Field``, would act as ``Argument``\ s. +**Note:** when using the ``Field`` constructor directly, pass the type and +not an instance. + + +If the types are mounted in a ``Field``, they act as ``Argument``\ s: .. code:: python From c1f567c4e4ba86cf4525dd2acbf1518cacb0d616 Mon Sep 17 00:00:00 2001 From: Tina Zhang Date: Sat, 5 Nov 2016 15:31:56 +0100 Subject: [PATCH 20/60] add link to official graphql website in Getting Started --- docs/quickstart.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 5a93ff30..5f664062 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,6 +1,12 @@ Getting started =============== +What is GraphQL? +---------------- + +For an introduction to GraphQL and an overview of its concepts, please refer +to `the official introduction `. + Let’s build a basic GraphQL schema from scratch. Requirements From d1a9bdb5c3d4c0bd04e165aff506d458b4ac93fe Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Mon, 7 Nov 2016 09:26:28 +1100 Subject: [PATCH 21/60] Add tox dependencies Tox fails without pytz, iso8601 and pytest-benchmark --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 7dbcffa5..03d61569 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,9 @@ deps= blinker singledispatch mock + pytz + iso8601 + pytest-benchmark setenv = PYTHONPATH = .:{envdir} commands= From 937e257d609c51ad81675168a38792b6083d0e2f Mon Sep 17 00:00:00 2001 From: Eran Kampf Date: Tue, 8 Nov 2016 13:42:59 -0800 Subject: [PATCH 22/60] Fix typo in Union initialization exception --- graphene/types/union.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/union.py b/graphene/types/union.py index fa178594..622f465e 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -42,4 +42,4 @@ class Union(six.with_metaclass(UnionMeta)): resolve_type = None def __init__(self, *args, **kwargs): - raise Exception("An Union cannot be intitialized") + raise Exception("A Union cannot be intitialized") From a427a0fb18b6d2c59893d7c9014b5d67c6866efd Mon Sep 17 00:00:00 2001 From: Timothy Laurent Date: Wed, 9 Nov 2016 22:27:08 -0800 Subject: [PATCH 23/60] Add test for _is_sunder and _is_dunder array.py functions. --- graphene/pyutils/enum.py | 12 ++++----- graphene/pyutils/tests/__init__.py | 0 graphene/pyutils/tests/test_enum.py | 41 +++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 graphene/pyutils/tests/__init__.py create mode 100644 graphene/pyutils/tests/test_enum.py diff --git a/graphene/pyutils/enum.py b/graphene/pyutils/enum.py index 8c09a6ad..928d0be2 100644 --- a/graphene/pyutils/enum.py +++ b/graphene/pyutils/enum.py @@ -71,18 +71,18 @@ def _is_descriptor(obj): def _is_dunder(name): """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[-3:-2] != '_' and - len(name) > 4) + name[-3:-2] != '_') def _is_sunder(name): """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[-2:-1] != '_' and - len(name) > 2) + name[-2:-1] != '_') def _make_class_unpicklable(cls): diff --git a/graphene/pyutils/tests/__init__.py b/graphene/pyutils/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/pyutils/tests/test_enum.py b/graphene/pyutils/tests/test_enum.py new file mode 100644 index 00000000..bf15a620 --- /dev/null +++ b/graphene/pyutils/tests/test_enum.py @@ -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 From 5f7af3e43f91e8c4da82bce8ef880871463b53cd Mon Sep 17 00:00:00 2001 From: markus Date: Thu, 10 Nov 2016 10:00:59 +0000 Subject: [PATCH 24/60] Make node arg options and default required to True for GlobalID. --- graphene/relay/node.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index c1bbe6d5..acaee3e4 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -35,9 +35,9 @@ def get_default_connection(cls): class GlobalID(Field): - def __init__(self, node, *args, **kwargs): - super(GlobalID, self).__init__(ID, *args, **kwargs) - self.node = node + def __init__(self, node=None, required=True, *args, **kwargs): + super(GlobalID, self).__init__(ID, required=required, *args, **kwargs) + self.node = node or Node @staticmethod def id_resolver(parent_resolver, node, root, args, context, info): @@ -52,7 +52,7 @@ class NodeMeta(InterfaceMeta): def __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 From 0a79df3d13a6aebf7f12b55a185b4aa7a2267430 Mon Sep 17 00:00:00 2001 From: markus Date: Thu, 10 Nov 2016 10:46:34 +0000 Subject: [PATCH 25/60] Added tests for global id. --- graphene/relay/tests/test_global_id.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 graphene/relay/tests/test_global_id.py diff --git a/graphene/relay/tests/test_global_id.py b/graphene/relay/tests/test_global_id.py new file mode 100644 index 00000000..d45b5da6 --- /dev/null +++ b/graphene/relay/tests/test_global_id.py @@ -0,0 +1,22 @@ +from ..node import Node, GlobalID + +from ...types import NonNull, ID + + +class CustomNode(Node): + + class Meta: + name = 'Node' + + +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 From 8fa6d61271b457bba54d0681e2fa4d1275890714 Mon Sep 17 00:00:00 2001 From: markus Date: Thu, 10 Nov 2016 10:54:03 +0000 Subject: [PATCH 26/60] 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 27/60] 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 28/60] 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 29/60] 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 30/60] 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 31/60] 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 32/60] 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 33/60] 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 34/60] 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 35/60] 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 36/60] 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 37/60] 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 38/60] 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 39/60] 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) From 6817761a0892f73f6b837115ff5ec0821392cf46 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 15 Nov 2016 22:31:46 -0800 Subject: [PATCH 40/60] Fixed Union and Interface resolve_type when the field is a List/NonNull --- graphene/types/tests/test_query.py | 95 ++++++++++++++++++++++++++++++ graphene/types/typemap.py | 14 ++--- 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index b1c5d112..daeb63e8 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -4,12 +4,14 @@ from functools import partial from graphql import Source, execute, parse, GraphQLError from ..field import Field +from ..interface import Interface from ..inputfield import InputField from ..inputobjecttype import InputObjectType from ..objecttype import ObjectType from ..scalars import Int, String from ..schema import Schema from ..structures import List +from ..union import Union from ..dynamic import Dynamic @@ -24,6 +26,99 @@ def test_query(): 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')) diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index d7a7f23e..3aa80145 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -30,18 +30,18 @@ def is_graphene_type(_type): 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) - # assert inspect.isclass(_type) and issubclass(_type, ObjectType), ( - # 'Received incompatible type "{}".'.format(_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): graphql_type = map.get(_type._meta.name) assert graphql_type and graphql_type.graphene_type == _type return graphql_type + return _type @@ -151,7 +151,7 @@ class TypeMap(GraphQLTypeMap): from .definitions import GrapheneInterfaceType _resolve_type = None 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( graphene_type=type, name=type._meta.name, @@ -178,7 +178,7 @@ class TypeMap(GraphQLTypeMap): from .definitions import GrapheneUnionType _resolve_type = None 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 = [] for i in type._meta.types: map = self.construct_objecttype(map, i) From 67c0872c9fceed737b2300d118e6950ee20e97fe Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 15 Nov 2016 22:41:10 -0800 Subject: [PATCH 41/60] Updated version to 1.1.1 --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index aa110279..9563ba13 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -10,7 +10,7 @@ except NameError: __SETUP__ = False -VERSION = (1, 1, 0, 'final', 0) +VERSION = (1, 1, 1, 'final', 0) __version__ = get_version(VERSION) From 11996e9121dc897ff67b0d4127d824136f06973b Mon Sep 17 00:00:00 2001 From: SangHee Kim Date: Mon, 21 Nov 2016 11:41:35 -0500 Subject: [PATCH 42/60] fixed: wrong example for interface --- docs/types/interfaces.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/types/interfaces.rst b/docs/types/interfaces.rst index ee0410a7..476d2bd3 100644 --- a/docs/types/interfaces.rst +++ b/docs/types/interfaces.rst @@ -23,14 +23,14 @@ This example model defines a Character, which has a name. ``Human`` and name = graphene.String() # Human is a Character implementation - class Human(ObjectType): + class Human(graphene.ObjectType): class Meta: interfaces = (Character, ) born_in = graphene.String() # Droid is a Character implementation - class Droid(Character): + class Droid(graphene.ObjectType): class Meta: interfaces = (Character, ) From dddb20a0f45b515ff11bd25cf14a9aafa81083c6 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Tue, 22 Nov 2016 18:04:22 -0500 Subject: [PATCH 43/60] added time type --- docs/types/scalars.rst | 3 ++- graphene/types/datetime.py | 32 +++++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index a75c70e4..7a825d36 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -9,9 +9,10 @@ Graphene defines the following base Scalar Types: - ``graphene.Boolean`` - ``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.Time`` - ``graphene.types.json.JSONString`` diff --git a/graphene/types/datetime.py b/graphene/types/datetime.py index 3dfbbb97..17987836 100644 --- a/graphene/types/datetime.py +++ b/graphene/types/datetime.py @@ -29,11 +29,37 @@ class DateTime(Scalar): ) return dt.isoformat() - @staticmethod - def parse_literal(node): + @classmethod + def parse_literal(cls, node): if isinstance(node, ast.StringValue): - return iso8601.parse_date(node.value) + return cls.parse_value(node.value) @staticmethod def parse_value(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.epocj_time, value)) + return datetime.time(dt.hour, dt.minute, dt.second, dt.microsecond, dt.tzinfo) From f089c78b997e2a588775360af2e12944b573cdd8 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Tue, 22 Nov 2016 18:08:48 -0500 Subject: [PATCH 44/60] added time type --- graphene/types/datetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/datetime.py b/graphene/types/datetime.py index 17987836..53043744 100644 --- a/graphene/types/datetime.py +++ b/graphene/types/datetime.py @@ -61,5 +61,5 @@ class Time(Scalar): @classmethod def parse_value(cls, value): - dt = iso8601.parse_date('{}T{}'.format(cls.epocj_time, value)) + dt = iso8601.parse_date('{}T{}'.format(cls.epoch_time, value)) return datetime.time(dt.hour, dt.minute, dt.second, dt.microsecond, dt.tzinfo) From 7c57d71e84cf43f47bda9cdeab14033e2adaed50 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 22 Nov 2016 22:35:24 -0800 Subject: [PATCH 45/60] Added a test case for covering InputObjectType as Argument --- graphene/types/tests/test_inputobjecttype.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/graphene/types/tests/test_inputobjecttype.py b/graphene/types/tests/test_inputobjecttype.py index ca73156c..7f8eaa7a 100644 --- a/graphene/types/tests/test_inputobjecttype.py +++ b/graphene/types/tests/test_inputobjecttype.py @@ -1,7 +1,9 @@ from ..abstracttype import AbstractType from ..field import Field +from ..argument import Argument from ..inputfield import InputField +from ..objecttype import ObjectType from ..inputobjecttype import InputObjectType from ..unmountedtype import UnmountedType @@ -61,6 +63,22 @@ def test_generate_inputobjecttype_unmountedtype(): 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(): class MyAbstractType(AbstractType): field1 = MyScalar(MyType) From cf8792ef9e051477cf04252d6799a7deb9d5028d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 22 Nov 2016 23:07:11 -0800 Subject: [PATCH 46/60] Improved Typemap importing --- graphene/types/typemap.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 3aa80145..204c3ed6 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -5,12 +5,15 @@ from functools import partial from graphql import (GraphQLArgument, GraphQLBoolean, GraphQLField, GraphQLFloat, GraphQLID, GraphQLInputObjectField, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLString) -from graphql.type import GraphQLEnumValue from graphql.execution.executor import get_default_resolve_type_fn +from graphql.type import GraphQLEnumValue 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.str_converters import to_camel_case +from .definitions import (GrapheneEnumType, GrapheneInputObjectType, + GrapheneInterfaceType, GrapheneObjectType, + GrapheneScalarType, GrapheneUnionType) from .dynamic import Dynamic from .enum import Enum from .field import Field @@ -83,7 +86,6 @@ class TypeMap(GraphQLTypeMap): return map def construct_scalar(self, map, type): - from .definitions import GrapheneScalarType _scalars = { String: GraphQLString, Int: GraphQLInt, @@ -106,7 +108,6 @@ class TypeMap(GraphQLTypeMap): return map def construct_enum(self, map, type): - from .definitions import GrapheneEnumType values = OrderedDict() for name, value in type._meta.enum.__members__.items(): values[name] = GraphQLEnumValue( @@ -124,7 +125,6 @@ class TypeMap(GraphQLTypeMap): return map def construct_objecttype(self, map, type): - from .definitions import GrapheneObjectType if type._meta.name in map: _type = map[type._meta.name] if is_graphene_type(_type): @@ -148,7 +148,6 @@ class TypeMap(GraphQLTypeMap): return map def construct_interface(self, map, type): - from .definitions import GrapheneInterfaceType _resolve_type = None if type.resolve_type: _resolve_type = partial(resolve_type, type.resolve_type, map, type._meta.name) @@ -164,7 +163,6 @@ class TypeMap(GraphQLTypeMap): return map def construct_inputobjecttype(self, map, type): - from .definitions import GrapheneInputObjectType map[type._meta.name] = GrapheneInputObjectType( graphene_type=type, name=type._meta.name, @@ -175,7 +173,6 @@ class TypeMap(GraphQLTypeMap): return map def construct_union(self, map, type): - from .definitions import GrapheneUnionType _resolve_type = None if type.resolve_type: _resolve_type = partial(resolve_type, type.resolve_type, map, type._meta.name) From df2900e215c88f78dc3fe213df78c8ea9832f3ea Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 22 Nov 2016 23:08:28 -0800 Subject: [PATCH 47/60] Removed schema.register --- graphene/types/schema.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index a57e6c37..e7f08c13 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -61,9 +61,6 @@ class Schema(GraphQLSchema): def execute(self, *args, **kwargs): return graphql(self, *args, **kwargs) - def register(self, object_type): - self.types.append(object_type) - def introspect(self): return self.execute(introspection_query).data From 5e0923b56073f42ce25da6628c46ebed120b0654 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 22 Nov 2016 23:34:52 -0800 Subject: [PATCH 48/60] Added type getter in the schema when accessing its attrs --- graphene/types/schema.py | 15 ++++++++ graphene/types/tests/test_schema.py | 54 +++++++++++++++++++++++++++++ graphene/types/typemap.py | 7 ++-- 3 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 graphene/types/tests/test_schema.py diff --git a/graphene/types/schema.py b/graphene/types/schema.py index e7f08c13..b95490ca 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -6,6 +6,7 @@ from graphql.type.introspection import IntrospectionSchema from graphql.utils.introspection_query import introspection_query from graphql.utils.schema_printer import print_schema +from .definitions import GrapheneGraphQLType from .typemap import TypeMap, is_graphene_type @@ -46,6 +47,20 @@ class Schema(GraphQLSchema): def get_subscription_type(self): 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): if not _type: return _type diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py new file mode 100644 index 00000000..af9bc14c --- /dev/null +++ b/graphene/types/tests/test_schema.py @@ -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() diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 204c3ed6..70aa84cc 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -13,7 +13,8 @@ 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) + GrapheneScalarType, GrapheneUnionType, + GrapheneGraphQLType) from .dynamic import Dynamic from .enum import Enum from .field import Field @@ -68,7 +69,7 @@ class TypeMap(GraphQLTypeMap): return self.reducer(map, type.of_type) if type._meta.name in map: _type = map[type._meta.name] - if is_graphene_type(_type): + if isinstance(_type, GrapheneGraphQLType): assert _type.graphene_type == type return map if issubclass(type, ObjectType): @@ -127,7 +128,7 @@ class TypeMap(GraphQLTypeMap): def construct_objecttype(self, map, type): if type._meta.name in map: _type = map[type._meta.name] - if is_graphene_type(_type): + if isinstance(_type, GrapheneGraphQLType): assert _type.graphene_type == type return map map[type._meta.name] = GrapheneObjectType( From a8bb6ae3487ec3cea42d04d441b7160bdb07e873 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 22 Nov 2016 23:41:59 -0800 Subject: [PATCH 49/60] Updated version to 1.1.2 --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index 9563ba13..a5df0d3f 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -10,7 +10,7 @@ except NameError: __SETUP__ = False -VERSION = (1, 1, 1, 'final', 0) +VERSION = (1, 1, 2, 'final', 0) __version__ = get_version(VERSION) From 48b422e2890e02801a6b3a993f28a0b696da797c Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Wed, 23 Nov 2016 11:12:28 -0500 Subject: [PATCH 50/60] add time query test --- graphene/types/tests/test_datetime.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index f55cd8c6..05dca4a5 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -1,18 +1,22 @@ import datetime import pytz -from ..datetime import DateTime +from ..datetime import DateTime, Time from ..objecttype import ObjectType from ..schema import Schema class Query(ObjectType): datetime = DateTime(_in=DateTime(name='in')) + time = Time(_at=Time(name='at')) def resolve_datetime(self, args, context, info): _in = args.get('in') return _in + def resolve_time(self, args, context, info): + return args.get('at') + 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(): now = datetime.datetime.now().replace(tzinfo=pytz.utc) isoformat = now.isoformat() From ad83a7e07694a48bb5ee8cbfe2b953150620fa31 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Wed, 23 Nov 2016 11:14:49 -0500 Subject: [PATCH 51/60] add time query test --- graphene/types/datetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/datetime.py b/graphene/types/datetime.py index 53043744..0a0e3343 100644 --- a/graphene/types/datetime.py +++ b/graphene/types/datetime.py @@ -61,5 +61,5 @@ class Time(Scalar): @classmethod def parse_value(cls, value): - dt = iso8601.parse_date('{}T{}'.format(cls.epoch_time, value)) + dt = iso8601.parse_date('{}T{}'.format(cls.epoch_date, value)) return datetime.time(dt.hour, dt.minute, dt.second, dt.microsecond, dt.tzinfo) From 4653a0e5df887affb101df9b84e90002c4890ea6 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Wed, 23 Nov 2016 11:19:24 -0500 Subject: [PATCH 52/60] add time query variable test --- graphene/types/tests/test_datetime.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index 05dca4a5..c19edad2 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -54,3 +54,18 @@ def test_datetime_query_variable(): assert result.data == { '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($date: Time){ time(at: $time) }''', + variable_values={'time': isoformat} + ) + assert not result.errors + assert result.data == { + 'time': isoformat + } From 20ca84966e8a0e67ba0c731b60182d192c407a10 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Wed, 23 Nov 2016 11:23:56 -0500 Subject: [PATCH 53/60] add time query variable test --- graphene/types/tests/test_datetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index c19edad2..cef7384f 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -62,7 +62,7 @@ def test_time_query_variable(): isoformat = time.isoformat() result = schema.execute( - '''query Test($date: Time){ time(at: $time) }''', + '''query Test($time: Time){ time(at: $time) }''', variable_values={'time': isoformat} ) assert not result.errors From 3df62d26a7962f1273df972da21b25ac75bc9cf3 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 1 Dec 2016 21:05:09 -0800 Subject: [PATCH 54/60] Fixed coroutines as relay connection resolvers Related PR #379 --- graphene/relay/connection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 339e4266..990d19cb 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -5,7 +5,7 @@ from functools import partial import six from graphql_relay import connection_from_list -from promise import Promise +from promise import Promise, is_thenable, promisify from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union) @@ -142,8 +142,8 @@ class IterableConnectionField(Field): resolved = resolver(root, args, context, info) on_resolve = partial(cls.resolve_connection, connection_type, args) - if isinstance(resolved, Promise): - return resolved.then(on_resolve) + if is_thenable(resolved): + return promisify(resolved).then(on_resolve) return on_resolve(resolved) From 80a809216b294f73d0ea44869a90a679570427cf Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 1 Dec 2016 21:07:58 -0800 Subject: [PATCH 55/60] Removed unused Promise import --- graphene/relay/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index 990d19cb..f85b675f 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -5,7 +5,7 @@ from functools import partial import six from graphql_relay import connection_from_list -from promise import Promise, is_thenable, promisify +from promise import is_thenable, promisify from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union) From 4a31c5a9273737d6b6cdf524785593b32e7a5d7d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 1 Dec 2016 21:10:55 -0800 Subject: [PATCH 56/60] Updated version to 1.1.3 --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index a5df0d3f..7a680edc 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -10,7 +10,7 @@ except NameError: __SETUP__ = False -VERSION = (1, 1, 2, 'final', 0) +VERSION = (1, 1, 3, 'final', 0) __version__ = get_version(VERSION) From da3683028e3049bf7b9e0d8327d11e45261a8e52 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 1 Dec 2016 21:18:37 -0800 Subject: [PATCH 57/60] Updated external dependencies --- setup.py | 6 +++--- tox.ini | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index b9f8d1f8..4c336451 100644 --- a/setup.py +++ b/setup.py @@ -70,9 +70,9 @@ setup( install_requires=[ 'six>=1.10.0', - 'graphql-core>=1.0', - 'graphql-relay>=0.4.4', - 'promise', + 'graphql-core>=1.0.1', + 'graphql-relay>=0.4.5', + 'promise>=1.0.1', ], tests_require=[ 'pytest>=2.7.2', diff --git a/tox.ini b/tox.ini index 03d61569..13d40c96 100644 --- a/tox.ini +++ b/tox.ini @@ -5,8 +5,8 @@ skipsdist = true [testenv] deps= pytest>=2.7.2 - graphql-core>=0.5.1 - graphql-relay>=0.4.3 + graphql-core>=1.0.1 + graphql-relay>=0.4.5 six blinker singledispatch From 774e9bf4639ca459d0a12a10e090d9ab671b6459 Mon Sep 17 00:00:00 2001 From: Jim van Eeden Date: Fri, 2 Dec 2016 15:35:43 +0100 Subject: [PATCH 58/60] fix so partial functions can be passed to field, solves issue #316 --- graphene/types/field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/field.py b/graphene/types/field.py index 3b9347c0..952189f8 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -60,7 +60,7 @@ class Field(OrderedType): @property 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 From 93dacda923069f6c944da93830fbf94b5ff33728 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 17 Dec 2016 16:24:28 -0800 Subject: [PATCH 59/60] Refactored mounted types logic --- graphene/types/argument.py | 6 +++--- graphene/types/dynamic.py | 4 ++-- graphene/types/field.py | 4 ++-- graphene/types/inputfield.py | 4 ++-- graphene/types/mountedtype.py | 21 +++++++++++++++++++++ graphene/types/unmountedtype.py | 24 ++++++------------------ graphene/types/utils.py | 30 +++++------------------------- 7 files changed, 41 insertions(+), 52 deletions(-) create mode 100644 graphene/types/mountedtype.py diff --git a/graphene/types/argument.py b/graphene/types/argument.py index 8a621b42..8d486f1b 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -1,12 +1,12 @@ from collections import OrderedDict from itertools import chain -from ..utils.orderedtype import OrderedType +from .mountedtype import MountedType 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): super(Argument, self).__init__(_creation_counter=_creation_counter) @@ -47,7 +47,7 @@ def to_arguments(args, extra_args=None): continue 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( diff --git a/graphene/types/dynamic.py b/graphene/types/dynamic.py index b7e2aaa1..c5aada20 100644 --- a/graphene/types/dynamic.py +++ b/graphene/types/dynamic.py @@ -1,9 +1,9 @@ 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 the schema. So we can have lazy fields. diff --git a/graphene/types/field.py b/graphene/types/field.py index 3b9347c0..7723df5a 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -2,8 +2,8 @@ import inspect from collections import Mapping, OrderedDict from functools import partial -from ..utils.orderedtype import OrderedType from .argument import Argument, to_arguments +from .mountedtype import MountedType from .structures import NonNull from .unmountedtype import UnmountedType @@ -18,7 +18,7 @@ def source_resolver(source, root, args, context, info): return resolved -class Field(OrderedType): +class Field(MountedType): def __init__(self, type, args=None, resolver=None, source=None, deprecation_reason=None, name=None, description=None, diff --git a/graphene/types/inputfield.py b/graphene/types/inputfield.py index 09ccf5a8..8bf5973e 100644 --- a/graphene/types/inputfield.py +++ b/graphene/types/inputfield.py @@ -1,8 +1,8 @@ -from ..utils.orderedtype import OrderedType +from .mountedtype import MountedType from .structures import NonNull -class InputField(OrderedType): +class InputField(MountedType): def __init__(self, type, name=None, default_value=None, deprecation_reason=None, description=None, diff --git a/graphene/types/mountedtype.py b/graphene/types/mountedtype.py new file mode 100644 index 00000000..e2d0b7a3 --- /dev/null +++ b/graphene/types/mountedtype.py @@ -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 + ) diff --git a/graphene/types/unmountedtype.py b/graphene/types/unmountedtype.py index e910421b..4dfe762c 100644 --- a/graphene/types/unmountedtype.py +++ b/graphene/types/unmountedtype.py @@ -27,41 +27,29 @@ class UnmountedType(OrderedType): ''' raise NotImplementedError("get_type not implemented in {}".format(self)) + def mount_as(self, _as): + return _as.mount(self) + def Field(self): # noqa: N802 ''' Mount the UnmountedType as Field ''' from .field import Field - return Field( - self.get_type(), - *self.args, - _creation_counter=self.creation_counter, - **self.kwargs - ) + return self.mount_as(Field) def InputField(self): # noqa: N802 ''' Mount the UnmountedType as InputField ''' from .inputfield import InputField - return InputField( - self.get_type(), - *self.args, - _creation_counter=self.creation_counter, - **self.kwargs - ) + return self.mount_as(InputField) def Argument(self): # noqa: N802 ''' Mount the UnmountedType as Argument ''' from .argument import Argument - return Argument( - self.get_type(), - *self.args, - _creation_counter=self.creation_counter, - **self.kwargs - ) + return self.mount_as(Argument) def __eq__(self, other): return ( diff --git a/graphene/types/utils.py b/graphene/types/utils.py index c171e9e0..3c70711c 100644 --- a/graphene/types/utils.py +++ b/graphene/types/utils.py @@ -1,8 +1,6 @@ from collections import OrderedDict -from .dynamic import Dynamic -from .field import Field -from .inputfield import InputField +from .mountedtype import MountedType from .unmountedtype import UnmountedType @@ -35,34 +33,16 @@ def get_base_fields(bases, _as=None): 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): ''' Get type mounted ''' - if isinstance(value, (Field, InputField, Dynamic)): + if isinstance(value, MountedType): return value 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): From edd090efdefef0f17b3caba9dc43edbdea4da5c9 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 17 Dec 2016 17:17:32 -0800 Subject: [PATCH 60/60] Added Mounted type tests --- graphene/types/tests/test_mountedtype.py | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 graphene/types/tests/test_mountedtype.py diff --git a/graphene/types/tests/test_mountedtype.py b/graphene/types/tests/test_mountedtype.py new file mode 100644 index 00000000..3df7e7fb --- /dev/null +++ b/graphene/types/tests/test_mountedtype.py @@ -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!'}