From a74c402f1344709364e489798277ff2d8dbe4982 Mon Sep 17 00:00:00 2001 From: hung-phan Date: Thu, 8 Dec 2016 16:51:37 +0000 Subject: [PATCH 01/58] Implement JSON type Remove duplicated test Remove unicode for python 3 Add more tests Add None case Update json type Remove unnecessary dependencies Add more test Add test --- graphene/types/jsontype.py | 39 +++++++++++ graphene/types/tests/test_jsontype.py | 94 +++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 graphene/types/jsontype.py create mode 100644 graphene/types/tests/test_jsontype.py diff --git a/graphene/types/jsontype.py b/graphene/types/jsontype.py new file mode 100644 index 00000000..fa46bd68 --- /dev/null +++ b/graphene/types/jsontype.py @@ -0,0 +1,39 @@ +from __future__ import unicode_literals + +from graphql.language.ast import (BooleanValue, FloatValue, IntValue, + StringValue, ListValue, ObjectValue) + +from graphene.types.scalars import MIN_INT, MAX_INT +from .scalars import Scalar + + +class JSON(Scalar): + """ + The `JSON` scalar type represents JSON values as specified by + [ECMA-404](http://www.ecma-international.org/ + publications/files/ECMA-ST/ECMA-404.pdf). + """ + + @staticmethod + def identity(value): + return value + + serialize = identity + parse_value = identity + + @staticmethod + def parse_literal(ast): + if isinstance(ast, (StringValue, BooleanValue)): + return ast.value + elif isinstance(ast, IntValue): + num = int(ast.value) + if MIN_INT <= num <= MAX_INT: + return num + elif isinstance(ast, FloatValue): + return float(ast.value) + elif isinstance(ast, ListValue): + return [JSON.parse_literal(value) for value in ast.values] + elif isinstance(ast, ObjectValue): + return {field.name.value: JSON.parse_literal(field.value) for field in ast.fields} + else: + return None diff --git a/graphene/types/tests/test_jsontype.py b/graphene/types/tests/test_jsontype.py new file mode 100644 index 00000000..382e79f6 --- /dev/null +++ b/graphene/types/tests/test_jsontype.py @@ -0,0 +1,94 @@ +from ..jsontype import JSON +from ..objecttype import ObjectType +from ..schema import Schema + + +class Query(ObjectType): + json = JSON(input=JSON()) + + def resolve_json(self, args, context, info): + input = args.get('input') + return input + + +schema = Schema(query=Query) + + +def test_json_query_variable(): + for json_value in [ + 1, + 1.1, + True, + 'str', + [1, 2, 3], + [1.1, 2.2, 3.3], + [True, False], + ['str1', 'str2'], + { + 'key_a': 'a', + 'key_b': 'b' + }, + { + 'int': 1, + 'float': 1.1, + 'boolean': True, + 'string': 'str', + 'int_list': [1, 2, 3], + 'float_list': [1.1, 2.2, 3.3], + 'boolean_list': [True, False], + 'string_list': ['str1', 'str2'], + 'nested_dict': { + 'key_a': 'a', + 'key_b': 'b' + } + }, + None + ]: + result = schema.execute( + '''query Test($json: JSON){ json(input: $json) }''', + variable_values={'json': json_value} + ) + assert not result.errors + assert result.data == { + 'json': json_value + } + + +def test_json_parse_literal_query(): + result = schema.execute( + ''' + query { + json(input: { + int: 1, + float: 1.1 + boolean: true, + string: "str", + int_list: [1, 2, 3], + float_list: [1.1, 2.2, 3.3], + boolean_list: [true, false] + string_list: ["str1", "str2"], + nested_dict: { + key_a: "a", + key_b: "b" + } + }) + } + ''' + ) + assert not result.errors + assert result.data == { + 'json': { + 'int': 1, + 'float': 1.1, + 'boolean': True, + 'string': 'str', + 'int_list': [1, 2, 3], + 'float_list': [1.1, 2.2, 3.3], + 'boolean_list': [True, False], + 'string_list': ['str1', 'str2'], + 'nested_dict': { + 'key_a': 'a', + 'key_b': 'b' + } + } + } From c200a088c07ebe0da67b799d2e3cb95d0bd1d6ad Mon Sep 17 00:00:00 2001 From: hung-phan Date: Thu, 8 Dec 2016 17:47:33 +0000 Subject: [PATCH 02/58] Add None case --- graphene/types/tests/test_jsontype.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/graphene/types/tests/test_jsontype.py b/graphene/types/tests/test_jsontype.py index 382e79f6..4308d9ac 100644 --- a/graphene/types/tests/test_jsontype.py +++ b/graphene/types/tests/test_jsontype.py @@ -70,7 +70,8 @@ def test_json_parse_literal_query(): nested_dict: { key_a: "a", key_b: "b" - } + }, + empty_key: null }) } ''' @@ -89,6 +90,7 @@ def test_json_parse_literal_query(): 'nested_dict': { 'key_a': 'a', 'key_b': 'b' - } + }, + 'empty_key': None } } From 17ea139ce4e33b739e0151779723c453e6345313 Mon Sep 17 00:00:00 2001 From: hung-phan Date: Thu, 8 Dec 2016 17:50:36 +0000 Subject: [PATCH 03/58] Update test --- graphene/types/tests/test_jsontype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/tests/test_jsontype.py b/graphene/types/tests/test_jsontype.py index 4308d9ac..b56fcf28 100644 --- a/graphene/types/tests/test_jsontype.py +++ b/graphene/types/tests/test_jsontype.py @@ -71,7 +71,7 @@ def test_json_parse_literal_query(): key_a: "a", key_b: "b" }, - empty_key: null + empty_key: undefined }) } ''' From 81c1cf3d61e47527ff49a15e97cfdbabe88a8755 Mon Sep 17 00:00:00 2001 From: Jonas Helfer Date: Wed, 15 Feb 2017 20:59:56 -0800 Subject: [PATCH 04/58] Update README.md Mention that Graphene works great with both Relay and Apollo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c77930b..c8ee5c8e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade to Graph [Graphene](http://graphene-python.org) 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 +- **Relay & Apollo:** Graphene has builtin support for both [Apollo](https://github.com/apollographql/apollo-client) and [Relay](https://github.com/facebook/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. From ecb1edd5c2924840dc35d83213ee35ecb782e18b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Feb 2017 12:50:58 -0800 Subject: [PATCH 05/58] Improved Docs --- README.md | 3 ++- README.rst | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8ee5c8e..920bb267 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade to Graph [Graphene](http://graphene-python.org) 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 & Apollo:** Graphene has builtin support for both [Apollo](https://github.com/apollographql/apollo-client) and [Relay](https://github.com/facebook/relay) +- **Relay:** Graphene has builtin support for both 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. @@ -25,6 +25,7 @@ Graphene has multiple integrations with different frameworks: | Google App Engine | [graphene-gae](https://github.com/graphql-python/graphene-gae/) | | Peewee | *In progress* ([Tracking Issue](https://github.com/graphql-python/graphene/issues/289)) | +Also, Graphene is fully compatible with the GraphQL spec, working seamlessly with all GraphQL clients, such as [Relay](https://github.com/facebook/relay), [Apollo](https://github.com/apollographql/apollo-client) and [gql](https://github.com/graphql-python/gql). ## Installation diff --git a/README.rst b/README.rst index e186a31d..7ba5e903 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ 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 +- **Relay:** Graphene has builtin support for both 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 @@ -34,6 +34,12 @@ Graphene has multiple integrations with different frameworks: | Peewee | *In progress* (`Tracking Issue `__) | +---------------------+----------------------------------------------------------------------------------------------+ +Also, Graphene is fully compatible with the GraphQL spec, working +seamlessly with all GraphQL clients, such as +`Relay `__, +`Apollo `__ and +`gql `__. + Installation ------------ From 2f87698a0b4951cdc685f22db0a28cae7090c08b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 02:35:30 -0800 Subject: [PATCH 06/58] Improved TypeMap and Dynamic Field to optionally include the schema --- graphene/types/dynamic.py | 7 ++- graphene/types/schema.py | 2 +- graphene/types/typemap.py | 98 ++++++++++++++++++++------------------- 3 files changed, 56 insertions(+), 51 deletions(-) diff --git a/graphene/types/dynamic.py b/graphene/types/dynamic.py index c5aada20..6c4092f0 100644 --- a/graphene/types/dynamic.py +++ b/graphene/types/dynamic.py @@ -9,10 +9,13 @@ class Dynamic(MountedType): the schema. So we can have lazy fields. ''' - def __init__(self, type, _creation_counter=None): + def __init__(self, type, with_schema=False, _creation_counter=None): super(Dynamic, self).__init__(_creation_counter=_creation_counter) assert inspect.isfunction(type) self.type = type + self.with_schema = with_schema - def get_type(self): + def get_type(self, schema=None): + if schema and self.with_schema: + return self.type(schema=schema) return self.type() diff --git a/graphene/types/schema.py b/graphene/types/schema.py index b95490ca..e9600dc9 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -94,4 +94,4 @@ class Schema(GraphQLSchema): ] if self.types: initial_types += self.types - self._type_map = TypeMap(initial_types, auto_camelcase=self.auto_camelcase) + self._type_map = TypeMap(initial_types, auto_camelcase=self.auto_camelcase, schema=self) diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 70aa84cc..7c71f35a 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -51,8 +51,9 @@ def resolve_type(resolve_type_func, map, type_name, root, context, info): class TypeMap(GraphQLTypeMap): - def __init__(self, types, auto_camelcase=True): + def __init__(self, types, auto_camelcase=True, schema=None): self.auto_camelcase = auto_camelcase + self.schema = schema super(TypeMap, self).__init__(types) def reducer(self, map, type): @@ -72,21 +73,25 @@ class TypeMap(GraphQLTypeMap): if isinstance(_type, GrapheneGraphQLType): assert _type.graphene_type == type return map + if issubclass(type, ObjectType): - return self.construct_objecttype(map, type) + internal_type = self.construct_objecttype(map, type) if issubclass(type, InputObjectType): - return self.construct_inputobjecttype(map, type) + internal_type = self.construct_inputobjecttype(map, type) if issubclass(type, Interface): - return self.construct_interface(map, type) + internal_type = self.construct_interface(map, type) if issubclass(type, Scalar): - return self.construct_scalar(map, type) + internal_type = self.construct_scalar(map, type) if issubclass(type, Enum): - return self.construct_enum(map, type) + internal_type = self.construct_enum(map, type) if issubclass(type, Union): - return self.construct_union(map, type) - return map + internal_type = self.construct_union(map, type) + + return GraphQLTypeMap.reducer(map, internal_type) def construct_scalar(self, map, type): + # We have a mapping to the original GraphQL types + # so there are no collisions. _scalars = { String: GraphQLString, Int: GraphQLInt, @@ -95,18 +100,17 @@ class TypeMap(GraphQLTypeMap): ID: GraphQLID } if type in _scalars: - map[type._meta.name] = _scalars[type] - else: - map[type._meta.name] = GrapheneScalarType( - graphene_type=type, - name=type._meta.name, - description=type._meta.description, + return _scalars[type] - serialize=getattr(type, 'serialize', None), - parse_value=getattr(type, 'parse_value', None), - parse_literal=getattr(type, 'parse_literal', None), - ) - return map + return GrapheneScalarType( + graphene_type=type, + name=type._meta.name, + description=type._meta.description, + + serialize=getattr(type, 'serialize', None), + parse_value=getattr(type, 'parse_value', None), + parse_literal=getattr(type, 'parse_literal', None), + ) def construct_enum(self, map, type): values = OrderedDict() @@ -117,61 +121,61 @@ class TypeMap(GraphQLTypeMap): description=getattr(value, 'description', None), deprecation_reason=getattr(value, 'deprecation_reason', None) ) - map[type._meta.name] = GrapheneEnumType( + return GrapheneEnumType( graphene_type=type, values=values, name=type._meta.name, description=type._meta.description, ) - return map def construct_objecttype(self, map, type): if type._meta.name in map: _type = map[type._meta.name] if isinstance(_type, GrapheneGraphQLType): assert _type.graphene_type == type - return map - map[type._meta.name] = GrapheneObjectType( + return _type + + def interfaces(): + interfaces = [] + for interface in type._meta.interfaces: + i = self.construct_interface(map, interface) + interfaces.append(i) + return interfaces + + return GrapheneObjectType( graphene_type=type, name=type._meta.name, description=type._meta.description, - fields=None, + fields=partial(self.construct_fields_for_type, map, type), is_type_of=type.is_type_of, - interfaces=None + interfaces=interfaces ) - interfaces = [] - for i in type._meta.interfaces: - map = self.reducer(map, i) - interfaces.append(map[i._meta.name]) - map[type._meta.name]._provided_interfaces = interfaces - map[type._meta.name]._fields = self.construct_fields_for_type(map, type) - # self.reducer(map, map[type._meta.name]) - return map def construct_interface(self, map, type): + if type._meta.name in map: + _type = map[type._meta.name] + if isinstance(_type, GrapheneInterfaceType): + assert _type.graphene_type == type + return _type + _resolve_type = None if type.resolve_type: _resolve_type = partial(resolve_type, type.resolve_type, map, type._meta.name) - map[type._meta.name] = GrapheneInterfaceType( + return GrapheneInterfaceType( graphene_type=type, name=type._meta.name, description=type._meta.description, - fields=None, + fields=partial(self.construct_fields_for_type, map, type), resolve_type=_resolve_type, ) - map[type._meta.name]._fields = self.construct_fields_for_type(map, type) - # self.reducer(map, map[type._meta.name]) - return map def construct_inputobjecttype(self, map, type): - map[type._meta.name] = GrapheneInputObjectType( + return GrapheneInputObjectType( graphene_type=type, name=type._meta.name, description=type._meta.description, - fields=None, + fields=partial(self.construct_fields_for_type, map, type, is_input_type=True), ) - map[type._meta.name]._fields = self.construct_fields_for_type(map, type, is_input_type=True) - return map def construct_union(self, map, type): _resolve_type = None @@ -179,16 +183,14 @@ class TypeMap(GraphQLTypeMap): _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) - types.append(map[i._meta.name]) - map[type._meta.name] = GrapheneUnionType( + internal_type = self.construct_objecttype(map, i) + types.append(internal_type) + return GrapheneUnionType( graphene_type=type, name=type._meta.name, types=types, resolve_type=_resolve_type, ) - map[type._meta.name].types = types - return map def get_name(self, name): if self.auto_camelcase: @@ -202,7 +204,7 @@ class TypeMap(GraphQLTypeMap): fields = OrderedDict() for name, field in type._meta.fields.items(): if isinstance(field, Dynamic): - field = get_field_as(field.get_type(), _as=Field) + field = get_field_as(field.get_type(self.schema), _as=Field) if not field: continue map = self.reducer(map, field.type) From 51b72d8dc8122405ea26a884072df909f085ba13 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 02:54:54 -0800 Subject: [PATCH 07/58] Renamed from JSON to Generic As it's implementation is abstract of the serializer/unserializer used. --- graphene/types/{jsontype.py => generic.py} | 12 +++++----- .../{test_jsontype.py => test_generic.py} | 22 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) rename graphene/types/{jsontype.py => generic.py} (71%) rename graphene/types/tests/{test_jsontype.py => test_generic.py} (81%) diff --git a/graphene/types/jsontype.py b/graphene/types/generic.py similarity index 71% rename from graphene/types/jsontype.py rename to graphene/types/generic.py index fa46bd68..5a8f467f 100644 --- a/graphene/types/jsontype.py +++ b/graphene/types/generic.py @@ -7,11 +7,11 @@ from graphene.types.scalars import MIN_INT, MAX_INT from .scalars import Scalar -class JSON(Scalar): +class Generic(Scalar): """ - The `JSON` scalar type represents JSON values as specified by - [ECMA-404](http://www.ecma-international.org/ - publications/files/ECMA-ST/ECMA-404.pdf). + The `Generic` scalar type represents a generic + GraphQL scalar value that could be: + String, Boolean, Int, Float, List or Object. """ @staticmethod @@ -32,8 +32,8 @@ class JSON(Scalar): elif isinstance(ast, FloatValue): return float(ast.value) elif isinstance(ast, ListValue): - return [JSON.parse_literal(value) for value in ast.values] + return [Generic.parse_literal(value) for value in ast.values] elif isinstance(ast, ObjectValue): - return {field.name.value: JSON.parse_literal(field.value) for field in ast.fields} + return {field.name.value: Generic.parse_literal(field.value) for field in ast.fields} else: return None diff --git a/graphene/types/tests/test_jsontype.py b/graphene/types/tests/test_generic.py similarity index 81% rename from graphene/types/tests/test_jsontype.py rename to graphene/types/tests/test_generic.py index b56fcf28..e5e940ba 100644 --- a/graphene/types/tests/test_jsontype.py +++ b/graphene/types/tests/test_generic.py @@ -1,12 +1,12 @@ -from ..jsontype import JSON +from ..generic import Generic from ..objecttype import ObjectType from ..schema import Schema class Query(ObjectType): - json = JSON(input=JSON()) + generic = Generic(input=Generic()) - def resolve_json(self, args, context, info): + def resolve_generic(self, args, context, info): input = args.get('input') return input @@ -14,8 +14,8 @@ class Query(ObjectType): schema = Schema(query=Query) -def test_json_query_variable(): - for json_value in [ +def test_generic_query_variable(): + for generic_value in [ 1, 1.1, True, @@ -45,20 +45,20 @@ def test_json_query_variable(): None ]: result = schema.execute( - '''query Test($json: JSON){ json(input: $json) }''', - variable_values={'json': json_value} + '''query Test($generic: Generic){ generic(input: $generic) }''', + variable_values={'generic': generic_value} ) assert not result.errors assert result.data == { - 'json': json_value + 'generic': generic_value } -def test_json_parse_literal_query(): +def test_generic_parse_literal_query(): result = schema.execute( ''' query { - json(input: { + generic(input: { int: 1, float: 1.1 boolean: true, @@ -78,7 +78,7 @@ def test_json_parse_literal_query(): ) assert not result.errors assert result.data == { - 'json': { + 'generic': { 'int': 1, 'float': 1.1, 'boolean': True, From ba29de567041d21cb4d033fb28364c085c9d3bdd Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 21:26:47 -0800 Subject: [PATCH 08/58] Improved assertion messages. Fixed #400 --- graphene/types/typemap.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 7c71f35a..918f5df8 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -43,7 +43,9 @@ def resolve_type(resolve_type_func, map, type_name, root, context, info): if inspect.isclass(_type) and issubclass(_type, ObjectType): graphql_type = map.get(_type._meta.name) - assert graphql_type and graphql_type.graphene_type == _type + assert graphql_type and graphql_type.graphene_type == _type, ( + 'The type {} does not match with the associated graphene type {}.' + ).format(_type, graphql_type.graphene_type) return graphql_type return _type @@ -71,7 +73,9 @@ class TypeMap(GraphQLTypeMap): if type._meta.name in map: _type = map[type._meta.name] if isinstance(_type, GrapheneGraphQLType): - assert _type.graphene_type == type + assert _type.graphene_type == type, ( + 'Found different types with the same name in the schema: {}, {}.' + ).format(_type.graphene_type, type) return map if issubclass(type, ObjectType): @@ -132,7 +136,9 @@ class TypeMap(GraphQLTypeMap): if type._meta.name in map: _type = map[type._meta.name] if isinstance(_type, GrapheneGraphQLType): - assert _type.graphene_type == type + assert _type.graphene_type == type, ( + 'Found different types with the same name in the schema: {}, {}.' + ).format(_type.graphene_type, type) return _type def interfaces(): @@ -155,7 +161,9 @@ class TypeMap(GraphQLTypeMap): if type._meta.name in map: _type = map[type._meta.name] if isinstance(_type, GrapheneInterfaceType): - assert _type.graphene_type == type + assert _type.graphene_type == type, ( + 'Found different types with the same name in the schema: {}, {}.' + ).format(_type.graphene_type, type) return _type _resolve_type = None From 0e2d9be6a8f1e8e841a3724b6503d25d8ab2efb1 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 21:41:11 -0800 Subject: [PATCH 09/58] Fixed test schema with empty query. Fixed #409 --- graphene/tests/issues/test_313.py | 5 ++++- graphene/types/schema.py | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/graphene/tests/issues/test_313.py b/graphene/tests/issues/test_313.py index bed87290..1a67a8ec 100644 --- a/graphene/tests/issues/test_313.py +++ b/graphene/tests/issues/test_313.py @@ -3,6 +3,9 @@ import graphene from graphene import resolve_only_args +class Query(graphene.ObjectType): + rand = graphene.String() + class Success(graphene.ObjectType): yeah = graphene.String() @@ -45,7 +48,7 @@ def test_create_post(): } ''' - schema = graphene.Schema(mutation=Mutations) + schema = graphene.Schema(query=Query, mutation=Mutations) result = schema.execute(query_string) assert not result.errors diff --git a/graphene/types/schema.py b/graphene/types/schema.py index e9600dc9..8066de3e 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -1,3 +1,4 @@ +import inspect from graphql import GraphQLSchema, graphql, is_type from graphql.type.directives import (GraphQLDirective, GraphQLIncludeDirective, @@ -7,6 +8,7 @@ from graphql.utils.introspection_query import introspection_query from graphql.utils.schema_printer import print_schema from .definitions import GrapheneGraphQLType +from .objecttype import ObjectType from .typemap import TypeMap, is_graphene_type @@ -20,6 +22,9 @@ class Schema(GraphQLSchema): def __init__(self, query=None, mutation=None, subscription=None, directives=None, types=None, auto_camelcase=True): + assert inspect.isclass(query) and issubclass(query, ObjectType), ( + 'Schema query must be Object Type but got: {}.' + ).format(query) self._query = query self._mutation = mutation self._subscription = subscription @@ -77,7 +82,10 @@ class Schema(GraphQLSchema): return graphql(self, *args, **kwargs) def introspect(self): - return self.execute(introspection_query).data + instrospection = self.execute(introspection_query) + if instrospection.errors: + raise instrospection.errors[0] + return instrospection.data def __str__(self): return print_schema(self) From 74400642ed8cc0b3d84fa326cf609de35f3b42fc Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 21:53:47 -0800 Subject: [PATCH 10/58] Added lazy_import to graphene. Fixed #316 --- graphene/__init__.py | 5 +++- graphene/utils/module_loading.py | 26 ++++++++++++++++++ graphene/utils/tests/test_module_loading.py | 29 +++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 graphene/utils/module_loading.py create mode 100644 graphene/utils/tests/test_module_loading.py diff --git a/graphene/__init__.py b/graphene/__init__.py index 7a680edc..cf1a3079 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -43,6 +43,7 @@ if not __SETUP__: PageInfo ) from .utils.resolve_only_args import resolve_only_args + from .utils.module_loading import lazy_import __all__ = [ 'AbstractType', @@ -72,4 +73,6 @@ if not __SETUP__: 'ClientIDMutation', 'Connection', 'ConnectionField', - 'PageInfo'] + 'PageInfo', + 'lazy_import', + ] diff --git a/graphene/utils/module_loading.py b/graphene/utils/module_loading.py new file mode 100644 index 00000000..8312cfb0 --- /dev/null +++ b/graphene/utils/module_loading.py @@ -0,0 +1,26 @@ +from functools import partial +from importlib import import_module + + +def import_string(dotted_path): + """ + Import a dotted module path and return the attribute/class designated by the + last name in the path. Raise ImportError if the import failed. + """ + try: + module_path, class_name = dotted_path.rsplit('.', 1) + except ValueError as err: + raise ImportError("%s doesn't look like a module path" % dotted_path) + + module = import_module(module_path) + + try: + return getattr(module, class_name) + except AttributeError as err: + raise ImportError('Module "%s" does not define a "%s" attribute/class' % ( + module_path, class_name) + ) + + +def lazy_import(dotted_path): + return partial(import_string, dotted_path) diff --git a/graphene/utils/tests/test_module_loading.py b/graphene/utils/tests/test_module_loading.py new file mode 100644 index 00000000..a0975f74 --- /dev/null +++ b/graphene/utils/tests/test_module_loading.py @@ -0,0 +1,29 @@ +from pytest import raises + +from graphene import String +from ..module_loading import lazy_import, import_string + + +def test_import_string(): + MyString = import_string('graphene.String') + assert MyString == String + + +def test_import_string_module(): + with raises(Exception) as exc_info: + import_string('graphenea') + + assert str(exc_info.value) == 'graphenea doesn\'t look like a module path' + + +def test_import_string_class(): + with raises(Exception) as exc_info: + import_string('graphene.Stringa') + + assert str(exc_info.value) == 'Module "graphene" does not define a "Stringa" attribute/class' + + +def test_lazy_import(): + f = lazy_import('graphene.String') + MyString = f() + assert MyString == String From b5cc0a81dad27f8f3f296e9f2c8460d8b8c50ed2 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 21:57:39 -0800 Subject: [PATCH 11/58] Improved scalar docs. Fixed #394 --- docs/types/scalars.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index ae195884..0958722d 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -72,4 +72,4 @@ Types mounted in a ``Field`` act as ``Argument``\ s. 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 1a52cf9a3d5d6b8c6159a53df6f5e57ad34066ed Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 21:59:38 -0800 Subject: [PATCH 12/58] Fixed lint --- graphene/utils/module_loading.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene/utils/module_loading.py b/graphene/utils/module_loading.py index 8312cfb0..cbbbb39d 100644 --- a/graphene/utils/module_loading.py +++ b/graphene/utils/module_loading.py @@ -9,14 +9,14 @@ def import_string(dotted_path): """ try: module_path, class_name = dotted_path.rsplit('.', 1) - except ValueError as err: + except ValueError: raise ImportError("%s doesn't look like a module path" % dotted_path) module = import_module(module_path) try: return getattr(module, class_name) - except AttributeError as err: + except AttributeError: raise ImportError('Module "%s" does not define a "%s" attribute/class' % ( module_path, class_name) ) From 8b0cd488d426dfec398a95f1c4b478ecee8c55cd Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 22:12:55 -0800 Subject: [PATCH 13/58] Improved Mutation docs. Fixed #402 --- docs/types/mutations.rst | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index 1155f1e8..11211593 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -10,19 +10,20 @@ This example defines a Mutation: .. code:: python - import graphene +import graphene - class CreatePerson(graphene.Mutation): - class Input: - name = graphene.String() +class CreatePerson(graphene.Mutation): + class Input: + name = graphene.String() - ok = graphene.Boolean() - person = graphene.Field(lambda: Person) + ok = graphene.Boolean() + person = graphene.Field(lambda: Person) - def mutate(self, args, context, info): - person = Person(name=args.get('name')) - ok = True - return CreatePerson(person=person, ok=ok) + @staticmethod + def mutate(root, args, context, info): + person = Person(name=args.get('name')) + ok = True + return CreatePerson(person=person, ok=ok) **person** and **ok** are the output fields of the Mutation when is resolved. @@ -42,11 +43,16 @@ So, we can finish our schema like this: class Person(graphene.ObjectType): name = graphene.String() + age = graphene.Int() class MyMutations(graphene.ObjectType): create_person = CreatePerson.Field() - schema = graphene.Schema(mutation=MyMutations) + # We must define a query for our schema + class Query(graphene.ObjectType): + person = graphene.Field(Person) + + schema = graphene.Schema(query=Query, mutation=MyMutations) Executing the Mutation ---------------------- @@ -96,11 +102,12 @@ To use an InputField you define an InputObjectType that specifies the structure class CreatePerson(graphene.Mutation): class Input: - person_data = graphene.InputField(PersonInput) + person_data = graphene.Argument(PersonInput) person = graphene.Field(lambda: Person) - def mutate(self, args, context, info): + @staticmethod + def mutate(root, args, context, info): p_data = args.get('person_data') name = p_data.get('name') From 11bed46a265316d1d18122d2f6dc7d83c5bd45a0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 22:22:54 -0800 Subject: [PATCH 14/58] Renamed Generic to GenericScalar --- graphene/types/generic.py | 8 ++++---- graphene/types/tests/test_generic.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/graphene/types/generic.py b/graphene/types/generic.py index 5a8f467f..a1034bfd 100644 --- a/graphene/types/generic.py +++ b/graphene/types/generic.py @@ -7,9 +7,9 @@ from graphene.types.scalars import MIN_INT, MAX_INT from .scalars import Scalar -class Generic(Scalar): +class GenericScalar(Scalar): """ - The `Generic` scalar type represents a generic + The `GenericScalar` scalar type represents a generic GraphQL scalar value that could be: String, Boolean, Int, Float, List or Object. """ @@ -32,8 +32,8 @@ class Generic(Scalar): elif isinstance(ast, FloatValue): return float(ast.value) elif isinstance(ast, ListValue): - return [Generic.parse_literal(value) for value in ast.values] + return [GenericScalar.parse_literal(value) for value in ast.values] elif isinstance(ast, ObjectValue): - return {field.name.value: Generic.parse_literal(field.value) for field in ast.fields} + return {field.name.value: GenericScalar.parse_literal(field.value) for field in ast.fields} else: return None diff --git a/graphene/types/tests/test_generic.py b/graphene/types/tests/test_generic.py index e5e940ba..aede1a8b 100644 --- a/graphene/types/tests/test_generic.py +++ b/graphene/types/tests/test_generic.py @@ -1,10 +1,10 @@ -from ..generic import Generic +from ..generic import GenericScalar from ..objecttype import ObjectType from ..schema import Schema class Query(ObjectType): - generic = Generic(input=Generic()) + generic = GenericScalar(input=GenericScalar()) def resolve_generic(self, args, context, info): input = args.get('input') @@ -45,7 +45,7 @@ def test_generic_query_variable(): None ]: result = schema.execute( - '''query Test($generic: Generic){ generic(input: $generic) }''', + '''query Test($generic: GenericScalar){ generic(input: $generic) }''', variable_values={'generic': generic_value} ) assert not result.errors From 81a2ed0bfe6d2c962a03c44d30469cd6f293ac3d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 22:28:42 -0800 Subject: [PATCH 15/58] Updated graphene version to 1.2.0 --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index cf1a3079..909370bb 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -10,7 +10,7 @@ except NameError: __SETUP__ = False -VERSION = (1, 1, 3, 'final', 0) +VERSION = (1, 2, 0, 'final', 0) __version__ = get_version(VERSION) From e2cdd80a5cc5524af0a45273fd257c08b0ff8bc2 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 22:44:27 -0800 Subject: [PATCH 16/58] Added docs for List and NonNull types. Fixed #326 --- docs/types/index.rst | 1 + docs/types/list-and-nonnull.rst | 50 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 docs/types/list-and-nonnull.rst diff --git a/docs/types/index.rst b/docs/types/index.rst index 41b34f27..1156b0ce 100644 --- a/docs/types/index.rst +++ b/docs/types/index.rst @@ -7,6 +7,7 @@ Types Reference enums scalars + list-and-nonnull interfaces abstracttypes objecttypes diff --git a/docs/types/list-and-nonnull.rst b/docs/types/list-and-nonnull.rst new file mode 100644 index 00000000..b48aa187 --- /dev/null +++ b/docs/types/list-and-nonnull.rst @@ -0,0 +1,50 @@ +Lists and Non-Null +================== + +Object types, scalars, and enums are the only kinds of types you can +define in Graphene. But when you use the types in other parts of the +schema, or in your query variable declarations, you can apply additional +type modifiers that affect validation of those values. + +NonNull +------- + +.. code:: python + + import graphene + + class Character(graphene.ObjectType): + name = graphene.NonNull(graphene.String) + + +Here, we're using a ``String`` type and marking it as Non-Null by wrapping +it using the ``NonNull`` class. This means that our server always expects +to return a non-null value for this field, and if it ends up getting a +null value that will actually trigger a GraphQL execution error, +letting the client know that something has gone wrong. + + +The previous ``NonNull`` code snippet is also equivalent to: + +.. code:: python + + import graphene + + class Character(graphene.ObjectType): + name = graphene.String(required=True) + + +List +---- + +.. code:: python + + import graphene + + class Character(graphene.ObjectType): + appears_in = graphene.List(graphene.String) + +Lists work in a similar way: We can use a type modifier to mark a type as a +``List``, which indicates that this field will return a list of that type. +It works the same for arguments, where the validation step will expect a list +for that value. From d1d87221d59263a97ca4ae1afc11d1e9f0be051e Mon Sep 17 00:00:00 2001 From: Wei Yen Date: Wed, 22 Feb 2017 06:55:47 +1100 Subject: [PATCH 17/58] Regression test to ensure docstring is trimmed Related to issue #418 --- graphene/types/tests/test_objecttype.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/graphene/types/tests/test_objecttype.py b/graphene/types/tests/test_objecttype.py index eb297917..8ccc0fac 100644 --- a/graphene/types/tests/test_objecttype.py +++ b/graphene/types/tests/test_objecttype.py @@ -173,3 +173,14 @@ def test_objecttype_container_benchmark(benchmark): @benchmark def create_objecttype(): Container(field1='field1', field2='field2') + + +def test_generate_objecttype_description(): + class MyObjectType(ObjectType): + ''' + Documentation + + Documentation line 2 + ''' + + assert MyObjectType._meta.description == "Documentation\n\nDocumentation line 2" From 0dc8e57c50b2245687e13a8be8de7b93bdff9bde Mon Sep 17 00:00:00 2001 From: Wei Yen Date: Wed, 22 Feb 2017 06:56:15 +1100 Subject: [PATCH 18/58] Implement `trim_docstring` Thin wrapper around `inspect.clean_doc` --- graphene/utils/tests/test_trim_docstring.py | 21 +++++++++++++++++++++ graphene/utils/trim_docstring.py | 9 +++++++++ 2 files changed, 30 insertions(+) create mode 100644 graphene/utils/tests/test_trim_docstring.py create mode 100644 graphene/utils/trim_docstring.py diff --git a/graphene/utils/tests/test_trim_docstring.py b/graphene/utils/tests/test_trim_docstring.py new file mode 100644 index 00000000..3aab5f11 --- /dev/null +++ b/graphene/utils/tests/test_trim_docstring.py @@ -0,0 +1,21 @@ +from ..trim_docstring import trim_docstring + + +def test_trim_docstring(): + class WellDocumentedObject(object): + """ + This object is very well-documented. It has multiple lines in its + description. + + Multiple paragraphs too + """ + pass + + assert (trim_docstring(WellDocumentedObject.__doc__) == + "This object is very well-documented. It has multiple lines in its\n" + "description.\n\nMultiple paragraphs too") + + class UndocumentedObject(object): + pass + + assert trim_docstring(UndocumentedObject.__doc__) is None diff --git a/graphene/utils/trim_docstring.py b/graphene/utils/trim_docstring.py new file mode 100644 index 00000000..a23c7e7d --- /dev/null +++ b/graphene/utils/trim_docstring.py @@ -0,0 +1,9 @@ +import inspect + + +def trim_docstring(docstring): + # Cleans up whitespaces from an indented docstring + # + # See https://www.python.org/dev/peps/pep-0257/ + # and https://docs.python.org/2/library/inspect.html#inspect.cleandoc + return inspect.cleandoc(docstring) if docstring else None From c592e94f73923ee2d2fbac26311e5976fcddee87 Mon Sep 17 00:00:00 2001 From: Wei Yen Date: Wed, 22 Feb 2017 06:58:14 +1100 Subject: [PATCH 19/58] Use trim_docstring to ensure description has no leading whitespace --- graphene/types/enum.py | 3 ++- graphene/types/inputobjecttype.py | 3 ++- graphene/types/interface.py | 3 ++- graphene/types/objecttype.py | 3 ++- graphene/types/scalars.py | 4 ++-- graphene/types/union.py | 3 ++- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 3bff137c..a6575cea 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -3,6 +3,7 @@ from collections import OrderedDict import six from ..utils.is_base_type import is_base_type +from ..utils.trim_docstring import trim_docstring from .options import Options from .unmountedtype import UnmountedType @@ -23,7 +24,7 @@ class EnumTypeMeta(type): options = Options( attrs.pop('Meta', None), name=name, - description=attrs.get('__doc__'), + description=trim_docstring(attrs.get('__doc__')), enum=None, ) if not options.enum: diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index cbc13f95..1796988a 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -1,6 +1,7 @@ import six from ..utils.is_base_type import is_base_type +from ..utils.trim_docstring import trim_docstring from .abstracttype import AbstractTypeMeta from .inputfield import InputField from .options import Options @@ -19,7 +20,7 @@ class InputObjectTypeMeta(AbstractTypeMeta): options = Options( attrs.pop('Meta', None), name=name, - description=attrs.get('__doc__'), + description=trim_docstring(attrs.get('__doc__')), local_fields=None, ) diff --git a/graphene/types/interface.py b/graphene/types/interface.py index cc8361e6..f0980b6c 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -1,6 +1,7 @@ import six from ..utils.is_base_type import is_base_type +from ..utils.trim_docstring import trim_docstring from .abstracttype import AbstractTypeMeta from .field import Field from .options import Options @@ -18,7 +19,7 @@ class InterfaceMeta(AbstractTypeMeta): options = Options( attrs.pop('Meta', None), name=name, - description=attrs.get('__doc__'), + description=trim_docstring(attrs.get('__doc__')), local_fields=None, ) diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index f06dbf5e..9b9e1005 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -3,6 +3,7 @@ from collections import OrderedDict import six from ..utils.is_base_type import is_base_type +from ..utils.trim_docstring import trim_docstring from .abstracttype import AbstractTypeMeta from .field import Field from .interface import Interface @@ -22,7 +23,7 @@ class ObjectTypeMeta(AbstractTypeMeta): options = _meta or Options( attrs.pop('Meta', None), name=name, - description=attrs.get('__doc__'), + description=trim_docstring(attrs.get('__doc__')), interfaces=(), local_fields=OrderedDict(), ) diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index 6f07c91c..e1ff80d3 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -1,9 +1,9 @@ import six - from graphql.language.ast import (BooleanValue, FloatValue, IntValue, StringValue) from ..utils.is_base_type import is_base_type +from ..utils.trim_docstring import trim_docstring from .options import Options from .unmountedtype import UnmountedType @@ -19,7 +19,7 @@ class ScalarTypeMeta(type): options = Options( attrs.pop('Meta', None), name=name, - description=attrs.get('__doc__'), + description=trim_docstring(attrs.get('__doc__')), ) return type.__new__(cls, name, bases, dict(attrs, _meta=options)) diff --git a/graphene/types/union.py b/graphene/types/union.py index e36086d0..d4af88ed 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -1,6 +1,7 @@ import six from ..utils.is_base_type import is_base_type +from ..utils.trim_docstring import trim_docstring from .options import Options from .unmountedtype import UnmountedType @@ -16,7 +17,7 @@ class UnionMeta(type): options = Options( attrs.pop('Meta', None), name=name, - description=attrs.get('__doc__'), + description=trim_docstring(attrs.get('__doc__')), types=(), ) From 47bb5292fa97a6307cbef3385a837a579502e72b Mon Sep 17 00:00:00 2001 From: Jonas Helfer Date: Wed, 22 Feb 2017 10:42:12 -0800 Subject: [PATCH 20/58] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 920bb267..efb6176a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade to Graph [Graphene](http://graphene-python.org) 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 both Relay. +- **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. From 98825fa4bc0f2fd6f88d11796b1093e0d63a51ee Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 23 Feb 2017 21:37:45 -0800 Subject: [PATCH 21/58] Added optional default_resolver to ObjectType. --- graphene/types/objecttype.py | 5 +-- graphene/types/resolver.py | 19 +++++++++++ graphene/types/tests/test_resolver.py | 48 +++++++++++++++++++++++++++ graphene/types/typemap.py | 7 ++-- 4 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 graphene/types/resolver.py create mode 100644 graphene/types/tests/test_resolver.py diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index 9b9e1005..b59256c7 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -19,12 +19,13 @@ class ObjectTypeMeta(AbstractTypeMeta): if not is_base_type(bases, ObjectTypeMeta): return type.__new__(cls, name, bases, attrs) - _meta = attrs.pop('_meta', None) - options = _meta or Options( + attrs.pop('_meta', None) + options = Options( attrs.pop('Meta', None), name=name, description=trim_docstring(attrs.get('__doc__')), interfaces=(), + default_resolver=None, local_fields=OrderedDict(), ) options.base_fields = get_base_fields(bases, _as=Field) diff --git a/graphene/types/resolver.py b/graphene/types/resolver.py new file mode 100644 index 00000000..1f395b50 --- /dev/null +++ b/graphene/types/resolver.py @@ -0,0 +1,19 @@ +def attr_resolver(attname, default_value, root, args, context, info): + return getattr(root, attname, default_value) + + +def dict_resolver(attname, default_value, root, args, context, info): + return root.get(attname, default_value) + + +default_resolver = attr_resolver + + +def set_default_resolver(resolver): + global default_resolver + assert callable(resolver), 'Received non-callable resolver.' + default_resolver = resolver + + +def get_default_resolver(): + return default_resolver diff --git a/graphene/types/tests/test_resolver.py b/graphene/types/tests/test_resolver.py new file mode 100644 index 00000000..25629979 --- /dev/null +++ b/graphene/types/tests/test_resolver.py @@ -0,0 +1,48 @@ +import pytest + +from ..resolver import attr_resolver, dict_resolver, get_default_resolver, set_default_resolver + +args = {} +context = None +info = None + +demo_dict = { + 'attr': 'value' +} + + +class demo_obj(object): + attr = 'value' + + +def test_attr_resolver(): + resolved = attr_resolver('attr', None, demo_obj, args, context, info) + assert resolved == 'value' + + +def test_attr_resolver_default_value(): + resolved = attr_resolver('attr2', 'default', demo_obj, args, context, info) + assert resolved == 'default' + + +def test_dict_resolver(): + resolved = dict_resolver('attr', None, demo_dict, args, context, info) + assert resolved == 'value' + + +def test_dict_resolver_default_value(): + resolved = dict_resolver('attr2', 'default', demo_dict, args, context, info) + assert resolved == 'default' + + +def test_get_default_resolver_is_attr_resolver(): + assert get_default_resolver() == attr_resolver + + +def test_set_default_resolver_workd(): + default_resolver = get_default_resolver() + + set_default_resolver(dict_resolver) + assert get_default_resolver() == dict_resolver + + set_default_resolver(default_resolver) diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 918f5df8..9dc17242 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -21,6 +21,7 @@ from .field import Field from .inputobjecttype import InputObjectType from .interface import Interface from .objecttype import ObjectType +from .resolver import get_default_resolver from .scalars import ID, Boolean, Float, Int, Scalar, String from .structures import List, NonNull from .union import Union @@ -205,9 +206,6 @@ class TypeMap(GraphQLTypeMap): return to_camel_case(name) return name - def default_resolver(self, attname, default_value, root, *_): - return getattr(root, attname, default_value) - def construct_fields_for_type(self, map, type, is_input_type=False): fields = OrderedDict() for name, field in type._meta.fields.items(): @@ -267,7 +265,8 @@ class TypeMap(GraphQLTypeMap): if resolver: return get_unbound_function(resolver) - return partial(self.default_resolver, name, default_value) + default_resolver = type._meta.default_resolver or get_default_resolver() + return partial(default_resolver, name, default_value) def get_field_type(self, map, type): if isinstance(type, List): From ad251e9a8dd46d212e39da67945df4fc3e02b361 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 26 Feb 2017 21:09:21 -0800 Subject: [PATCH 22/58] Fix ObjectType special types. Fix #425 --- graphene/tests/issues/test_425.py | 53 +++++++++++++++++++++++++++++++ graphene/types/objecttype.py | 3 +- graphene/types/options.py | 7 ++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 graphene/tests/issues/test_425.py diff --git a/graphene/tests/issues/test_425.py b/graphene/tests/issues/test_425.py new file mode 100644 index 00000000..95a1d79d --- /dev/null +++ b/graphene/tests/issues/test_425.py @@ -0,0 +1,53 @@ +# https://github.com/graphql-python/graphene/issues/425 +import six + +from graphene.utils.is_base_type import is_base_type + +from graphene.types.objecttype import ObjectTypeMeta, ObjectType +from graphene.types.options import Options + +class SpecialObjectTypeMeta(ObjectTypeMeta): + + @staticmethod + def __new__(cls, name, bases, attrs): + # Also ensure initialization is only performed for subclasses of + # DjangoObjectType + if not is_base_type(bases, SpecialObjectTypeMeta): + return type.__new__(cls, name, bases, attrs) + + options = Options( + attrs.pop('Meta', None), + other_attr='default', + ) + + return ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options)) + + return cls + + +class SpecialObjectType(six.with_metaclass(SpecialObjectTypeMeta, ObjectType)): + pass + + +def test_special_objecttype_could_be_subclassed(): + class MyType(SpecialObjectType): + class Meta: + other_attr = 'yeah!' + + assert MyType._meta.other_attr == 'yeah!' + + +def test_special_objecttype_could_be_subclassed_default(): + class MyType(SpecialObjectType): + pass + + assert MyType._meta.other_attr == 'default' + + +def test_special_objecttype_inherit_meta_options(): + class MyType(SpecialObjectType): + pass + + assert MyType._meta.name == 'MyType' + assert MyType._meta.default_resolver == None + assert MyType._meta.interfaces == () diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index b59256c7..b92af9e5 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -19,7 +19,7 @@ class ObjectTypeMeta(AbstractTypeMeta): if not is_base_type(bases, ObjectTypeMeta): return type.__new__(cls, name, bases, attrs) - attrs.pop('_meta', None) + _meta = attrs.pop('_meta', None) options = Options( attrs.pop('Meta', None), name=name, @@ -28,6 +28,7 @@ class ObjectTypeMeta(AbstractTypeMeta): default_resolver=None, local_fields=OrderedDict(), ) + options.extend_with_meta(_meta) options.base_fields = get_base_fields(bases, _as=Field) if not options.local_fields: diff --git a/graphene/types/options.py b/graphene/types/options.py index 0002db68..43f7154b 100644 --- a/graphene/types/options.py +++ b/graphene/types/options.py @@ -30,6 +30,13 @@ class Options(object): ) ) + def extend_with_meta(self, meta): + if not meta: + return + meta_attrs = props(meta) + for attr_name, value in meta_attrs.items(): + setattr(self, attr_name, value) + def __repr__(self): options_props = props(self) props_as_attrs = ' '.join(['{}={}'.format(key, value) for key, value in options_props.items()]) From e405a20e3dad7862ae315077150f23b63293a8ea Mon Sep 17 00:00:00 2001 From: nattyait Date: Wed, 1 Mar 2017 16:11:04 +0700 Subject: [PATCH 23/58] Add Sphinx to requirements for building graphene docs --- docs/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 5de8cc6b..31a23482 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,4 @@ +# Required library +Sphinx==1.5.3 # Docs template https://github.com/graphql-python/graphene-python.org/archive/docs.zip From 3a198d052acc01a8469dc03d0c0d798612830be2 Mon Sep 17 00:00:00 2001 From: Chapman Siu Date: Wed, 1 Mar 2017 22:54:06 +1100 Subject: [PATCH 24/58] fixed indent in mutation docs --- docs/types/mutations.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index 11211593..d7f590a4 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -10,20 +10,20 @@ This example defines a Mutation: .. code:: python -import graphene + import graphene -class CreatePerson(graphene.Mutation): - class Input: - name = graphene.String() + class CreatePerson(graphene.Mutation): + class Input: + name = graphene.String() - ok = graphene.Boolean() - person = graphene.Field(lambda: Person) + ok = graphene.Boolean() + person = graphene.Field(lambda: Person) - @staticmethod - def mutate(root, args, context, info): - person = Person(name=args.get('name')) - ok = True - return CreatePerson(person=person, ok=ok) + @staticmethod + def mutate(root, args, context, info): + person = Person(name=args.get('name')) + ok = True + return CreatePerson(person=person, ok=ok) **person** and **ok** are the output fields of the Mutation when is resolved. From 313a0041414c729fba8c2270359628b1251ce960 Mon Sep 17 00:00:00 2001 From: Ryan Wilson-Perkin Date: Wed, 1 Mar 2017 09:30:39 -0500 Subject: [PATCH 25/58] Spelling/Grammar fixes in docs Hi, thanks for writing these docs! I'm reading through them and hoping to give back by fixing up some spelling and grammatical issues. --- docs/relay/nodes.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/relay/nodes.rst b/docs/relay/nodes.rst index 59c818da..4b2c96aa 100644 --- a/docs/relay/nodes.rst +++ b/docs/relay/nodes.rst @@ -3,7 +3,7 @@ Nodes A ``Node`` is an Interface provided by ``graphene.relay`` that contains a single field ``id`` (which is a ``ID!``). Any object that inherits -from it have to implement a ``get_node`` method for retrieving a +from it has to implement a ``get_node`` method for retrieving a ``Node`` by an *id*. @@ -26,8 +26,8 @@ Example usage (taken from the `Starwars Relay example`_): return get_ship(id) The ``id`` returned by the ``Ship`` type when you query it will be a -scalar which contains the enough info for the server for knowing it’s -type and it’s id. +scalar which contains enough info for the server to know its type and +its id. For example, the instance ``Ship(id=1)`` will return ``U2hpcDox`` as the id when you query it (which is the base64 encoding of ``Ship:1``), and @@ -77,7 +77,7 @@ Accessing node types If we want to retrieve node instances from a ``global_id`` (scalar that identifies an instance by it's type name and id), we can simply do ``Node.get_node_from_global_id(global_id, context, info)``. -In the case we want to restrict the instnance retrieval to an specific type, we can do: +In the case we want to restrict the instance retrieval to a specific type, we can do: ``Node.get_node_from_global_id(global_id, context, info, only_type=Ship)``. This will raise an error if the ``global_id`` doesn't correspond to a Ship type. From 48efec307ce245bfe692f75b5dc07c14d8bce15b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 4 Mar 2017 18:11:51 -0800 Subject: [PATCH 26/58] Improved Options merge attrs. Fixed #432 --- graphene/tests/issues/test_425.py | 4 ++-- graphene/types/objecttype.py | 12 +++++++++--- graphene/types/options.py | 11 +++++------ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/graphene/tests/issues/test_425.py b/graphene/tests/issues/test_425.py index 95a1d79d..08bdde8c 100644 --- a/graphene/tests/issues/test_425.py +++ b/graphene/tests/issues/test_425.py @@ -20,8 +20,8 @@ class SpecialObjectTypeMeta(ObjectTypeMeta): other_attr='default', ) - return ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options)) - + cls = ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options)) + assert cls._meta is options return cls diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index b92af9e5..bedc5e76 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -20,15 +20,21 @@ class ObjectTypeMeta(AbstractTypeMeta): return type.__new__(cls, name, bases, attrs) _meta = attrs.pop('_meta', None) - options = Options( - attrs.pop('Meta', None), + defaults = dict( name=name, description=trim_docstring(attrs.get('__doc__')), interfaces=(), default_resolver=None, local_fields=OrderedDict(), ) - options.extend_with_meta(_meta) + if not _meta: + options = Options( + attrs.pop('Meta', None), + **defaults + ) + else: + options = _meta.extend_with_defaults(defaults) + options.base_fields = get_base_fields(bases, _as=Field) if not options.local_fields: diff --git a/graphene/types/options.py b/graphene/types/options.py index 43f7154b..7cefbea0 100644 --- a/graphene/types/options.py +++ b/graphene/types/options.py @@ -30,12 +30,11 @@ class Options(object): ) ) - def extend_with_meta(self, meta): - if not meta: - return - meta_attrs = props(meta) - for attr_name, value in meta_attrs.items(): - setattr(self, attr_name, value) + def extend_with_defaults(self, defaults): + for attr_name, value in defaults.items(): + if not hasattr(self, attr_name): + setattr(self, attr_name, value) + return self def __repr__(self): options_props = props(self) From 360d5cfac5f8ae1f6c2da2b145a247a7c6d92058 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 4 Mar 2017 18:17:53 -0800 Subject: [PATCH 27/58] Fixed test instructions --- README.md | 2 +- README.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index efb6176a..e2d79edf 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ If you want to learn even more, you can also check the following [examples](exam After cloning this repo, ensure dependencies are installed by running: ```sh -pip install .[test] +pip install -e ".[test]" ``` After developing, the full test suite can be evaluated by running: diff --git a/README.rst b/README.rst index 7ba5e903..c0fa1c28 100644 --- a/README.rst +++ b/README.rst @@ -95,7 +95,7 @@ After cloning this repo, ensure dependencies are installed by running: .. code:: sh - pip install .[test] + pip install -e ".[test]" After developing, the full test suite can be evaluated by running: From d86bbd8a8647bbcf9d668968977cc35c64f67f12 Mon Sep 17 00:00:00 2001 From: Grigoriy Beziuk Date: Fri, 10 Mar 2017 11:05:01 +0300 Subject: [PATCH 28/58] a fypo fixed so the hyperlink to the graphQL official introduction is shown now --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index c72259c1..09dddb6f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -5,7 +5,7 @@ What is GraphQL? ---------------- For an introduction to GraphQL and an overview of its concepts, please refer -to `the official introduction `. +to `the official introduction `_. Let’s build a basic GraphQL schema from scratch. From d4d8a76a0936b8b8b90e6b316003c31c21e30500 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 24 Mar 2017 22:40:16 -0700 Subject: [PATCH 29/58] Fixed circular union. Fixed #439 --- graphene/types/typemap.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 9dc17242..16a9b3ca 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -81,16 +81,18 @@ class TypeMap(GraphQLTypeMap): if issubclass(type, ObjectType): internal_type = self.construct_objecttype(map, type) - if issubclass(type, InputObjectType): + elif issubclass(type, InputObjectType): internal_type = self.construct_inputobjecttype(map, type) - if issubclass(type, Interface): + elif issubclass(type, Interface): internal_type = self.construct_interface(map, type) - if issubclass(type, Scalar): + elif issubclass(type, Scalar): internal_type = self.construct_scalar(map, type) - if issubclass(type, Enum): + elif issubclass(type, Enum): internal_type = self.construct_enum(map, type) - if issubclass(type, Union): + elif issubclass(type, Union): internal_type = self.construct_union(map, type) + else: + raise Exception("Expected Graphene type, but received: {}.".format(type)) return GraphQLTypeMap.reducer(map, internal_type) @@ -145,8 +147,10 @@ class TypeMap(GraphQLTypeMap): def interfaces(): interfaces = [] for interface in type._meta.interfaces: - i = self.construct_interface(map, interface) - interfaces.append(i) + self.graphene_reducer(map, interface) + internal_type = map[interface._meta.name] + assert internal_type.graphene_type == interface + interfaces.append(internal_type) return interfaces return GrapheneObjectType( @@ -190,10 +194,16 @@ class TypeMap(GraphQLTypeMap): _resolve_type = None if type.resolve_type: _resolve_type = partial(resolve_type, type.resolve_type, map, type._meta.name) - types = [] - for i in type._meta.types: - internal_type = self.construct_objecttype(map, i) - types.append(internal_type) + + def types(): + union_types = [] + for objecttype in type._meta.types: + self.graphene_reducer(map, objecttype) + internal_type = map[objecttype._meta.name] + assert internal_type.graphene_type == objecttype + union_types.append(internal_type) + return union_types + return GrapheneUnionType( graphene_type=type, name=type._meta.name, From 087f1c55cd99d591ac99595beed9ac8c2e8f3fe5 Mon Sep 17 00:00:00 2001 From: Yue Gong Date: Sat, 25 Mar 2017 17:41:47 +0800 Subject: [PATCH 30/58] Improve lazy_import() to accept dotted_attributes --- graphene/utils/module_loading.py | 23 ++++++++++++++--- graphene/utils/tests/test_module_loading.py | 28 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/graphene/utils/module_loading.py b/graphene/utils/module_loading.py index cbbbb39d..76c68bcb 100644 --- a/graphene/utils/module_loading.py +++ b/graphene/utils/module_loading.py @@ -2,7 +2,7 @@ from functools import partial from importlib import import_module -def import_string(dotted_path): +def import_string(dotted_path, dotted_attributes=None): """ Import a dotted module path and return the attribute/class designated by the last name in the path. Raise ImportError if the import failed. @@ -15,12 +15,27 @@ def import_string(dotted_path): module = import_module(module_path) try: - return getattr(module, class_name) + result = getattr(module, class_name) except AttributeError: raise ImportError('Module "%s" does not define a "%s" attribute/class' % ( module_path, class_name) ) + if not dotted_attributes: + return result + else: + attributes = dotted_attributes.split('.') + traveled_attributes = [] + try: + for attribute in attributes: + traveled_attributes.append(attribute) + result = getattr(result, attribute) + return result + except AttributeError: + raise ImportError('Module "%s" does not define a "%s" attribute inside attribute/class "%s"' % ( + module_path, '.'.join(traveled_attributes), class_name + )) -def lazy_import(dotted_path): - return partial(import_string, dotted_path) + +def lazy_import(dotted_path, dotted_attributes=None): + return partial(import_string, dotted_path, dotted_attributes) diff --git a/graphene/utils/tests/test_module_loading.py b/graphene/utils/tests/test_module_loading.py index a0975f74..769fde8b 100644 --- a/graphene/utils/tests/test_module_loading.py +++ b/graphene/utils/tests/test_module_loading.py @@ -1,6 +1,7 @@ from pytest import raises from graphene import String +from graphene.types.objecttype import ObjectTypeMeta from ..module_loading import lazy_import, import_string @@ -8,6 +9,9 @@ def test_import_string(): MyString = import_string('graphene.String') assert MyString == String + MyObjectTypeMeta = import_string('graphene.ObjectType', '__class__') + assert MyObjectTypeMeta == ObjectTypeMeta + def test_import_string_module(): with raises(Exception) as exc_info: @@ -23,7 +27,31 @@ def test_import_string_class(): assert str(exc_info.value) == 'Module "graphene" does not define a "Stringa" attribute/class' +def test_import_string_attributes(): + with raises(Exception) as exc_info: + import_string('graphene.String', 'length') + + assert str(exc_info.value) == 'Module "graphene" does not define a "length" attribute inside attribute/class ' \ + '"String"' + + with raises(Exception) as exc_info: + import_string('graphene.ObjectType', '__class__.length') + + assert str(exc_info.value) == 'Module "graphene" does not define a "__class__.length" attribute inside ' \ + 'attribute/class "ObjectType"' + + with raises(Exception) as exc_info: + import_string('graphene.ObjectType', '__classa__.__base__') + + assert str(exc_info.value) == 'Module "graphene" does not define a "__classa__" attribute inside attribute/class ' \ + '"ObjectType"' + + def test_lazy_import(): f = lazy_import('graphene.String') MyString = f() assert MyString == String + + f = lazy_import('graphene.ObjectType', '__class__') + MyObjectTypeMeta = f() + assert MyObjectTypeMeta == ObjectTypeMeta From 3822d6568e7617700dc056c018541a24be2bd1e5 Mon Sep 17 00:00:00 2001 From: Yue Gong Date: Sat, 25 Mar 2017 17:59:01 +0800 Subject: [PATCH 31/58] Update module_loading documents --- graphene/utils/module_loading.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/graphene/utils/module_loading.py b/graphene/utils/module_loading.py index 76c68bcb..f544cf24 100644 --- a/graphene/utils/module_loading.py +++ b/graphene/utils/module_loading.py @@ -5,7 +5,10 @@ from importlib import import_module def import_string(dotted_path, dotted_attributes=None): """ Import a dotted module path and return the attribute/class designated by the - last name in the path. Raise ImportError if the import failed. + last name in the path. When a dotted attribute path is also provided, the + dotted attribute path would be applied to the attribute/class retrieved from + the first step, and return the corresponding value designated by the + attribute path. Raise ImportError if the import failed. """ try: module_path, class_name = dotted_path.rsplit('.', 1) From bd0d418986cf69b51801f8e4d361c0c9a5dbc17c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 5 Apr 2017 16:27:17 -0700 Subject: [PATCH 32/58] Fixed promisify --- graphene/relay/connection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index f85b675f..be77700d 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 is_thenable, promisify +from promise import Promise, is_thenable from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union) @@ -143,7 +143,7 @@ class IterableConnectionField(Field): on_resolve = partial(cls.resolve_connection, connection_type, args) if is_thenable(resolved): - return promisify(resolved).then(on_resolve) + return Promise.resolve(resolved).then(on_resolve) return on_resolve(resolved) From 4b714659225a93ea44e95ccdd9ad1b5ea9e36c05 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 6 Apr 2017 22:13:06 -0700 Subject: [PATCH 33/58] Improved lazy types support in Graphene This commit also adds support for string types in Field, InputField, List and NonNull, where the string will be import. Usage like: Field("graphene.String") --- graphene/types/field.py | 5 ++-- graphene/types/inputfield.py | 7 ++++- graphene/types/structures.py | 7 ++++- graphene/types/tests/test_field.py | 13 +++++++++ graphene/types/tests/test_inputfield.py | 30 +++++++++++++++++++++ graphene/types/tests/test_structures.py | 36 +++++++++++++++++++++++++ graphene/types/tests/utils.py | 1 + graphene/types/typemap.py | 2 -- graphene/types/utils.py | 12 +++++++++ 9 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 graphene/types/tests/test_inputfield.py create mode 100644 graphene/types/tests/utils.py diff --git a/graphene/types/field.py b/graphene/types/field.py index 7e603852..06632d35 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -6,6 +6,7 @@ from .argument import Argument, to_arguments from .mountedtype import MountedType from .structures import NonNull from .unmountedtype import UnmountedType +from .utils import get_type base_type = type @@ -60,9 +61,7 @@ class Field(MountedType): @property def type(self): - if inspect.isfunction(self._type) or type(self._type) is partial: - return self._type() - return self._type + return get_type(self._type) def get_resolver(self, parent_resolver): return self.resolver or parent_resolver diff --git a/graphene/types/inputfield.py b/graphene/types/inputfield.py index 8bf5973e..0510ab4a 100644 --- a/graphene/types/inputfield.py +++ b/graphene/types/inputfield.py @@ -1,5 +1,6 @@ from .mountedtype import MountedType from .structures import NonNull +from .utils import get_type class InputField(MountedType): @@ -11,7 +12,11 @@ class InputField(MountedType): self.name = name if required: type = NonNull(type) - self.type = type + self._type = type self.deprecation_reason = deprecation_reason self.default_value = default_value self.description = description + + @property + def type(self): + return get_type(self._type) diff --git a/graphene/types/structures.py b/graphene/types/structures.py index 1ecfa83d..38fa5609 100644 --- a/graphene/types/structures.py +++ b/graphene/types/structures.py @@ -1,4 +1,5 @@ from .unmountedtype import UnmountedType +from .utils import get_type class Structure(UnmountedType): @@ -18,7 +19,11 @@ class Structure(UnmountedType): cls_name, of_type_name, )) - self.of_type = of_type + self._of_type = of_type + + @property + def of_type(self): + return get_type(self._of_type) def get_type(self): ''' diff --git a/graphene/types/tests/test_field.py b/graphene/types/tests/test_field.py index e4ef03bf..80a32154 100644 --- a/graphene/types/tests/test_field.py +++ b/graphene/types/tests/test_field.py @@ -1,9 +1,11 @@ import pytest +from functools import partial from ..argument import Argument from ..field import Field from ..structures import NonNull from ..scalars import String +from .utils import MyLazyType class MyInstance(object): @@ -66,6 +68,17 @@ def test_field_with_lazy_type(): assert field.type == MyType +def test_field_with_lazy_partial_type(): + MyType = object() + field = Field(partial(lambda: MyType)) + assert field.type == MyType + + +def test_field_with_string_type(): + field = Field("graphene.types.tests.utils.MyLazyType") + assert field.type == MyLazyType + + def test_field_not_source_and_resolver(): MyType = object() with pytest.raises(Exception) as exc_info: diff --git a/graphene/types/tests/test_inputfield.py b/graphene/types/tests/test_inputfield.py new file mode 100644 index 00000000..a0888e44 --- /dev/null +++ b/graphene/types/tests/test_inputfield.py @@ -0,0 +1,30 @@ +import pytest +from functools import partial + +from ..inputfield import InputField +from ..structures import NonNull +from .utils import MyLazyType + + +def test_inputfield_required(): + MyType = object() + field = InputField(MyType, required=True) + assert isinstance(field.type, NonNull) + assert field.type.of_type == MyType + + +def test_inputfield_with_lazy_type(): + MyType = object() + field = InputField(lambda: MyType) + assert field.type == MyType + + +def test_inputfield_with_lazy_partial_type(): + MyType = object() + field = InputField(partial(lambda: MyType)) + assert field.type == MyType + + +def test_inputfield_with_string_type(): + field = InputField("graphene.types.tests.utils.MyLazyType") + assert field.type == MyLazyType diff --git a/graphene/types/tests/test_structures.py b/graphene/types/tests/test_structures.py index e45f09e2..082bf097 100644 --- a/graphene/types/tests/test_structures.py +++ b/graphene/types/tests/test_structures.py @@ -1,7 +1,9 @@ import pytest +from functools import partial from ..structures import List, NonNull from ..scalars import String +from .utils import MyLazyType def test_list(): @@ -17,6 +19,23 @@ def test_list_with_unmounted_type(): assert str(exc_info.value) == 'List could not have a mounted String() as inner type. Try with List(String).' +def test_list_with_lazy_type(): + MyType = object() + field = List(lambda: MyType) + assert field.of_type == MyType + + +def test_list_with_lazy_partial_type(): + MyType = object() + field = List(partial(lambda: MyType)) + assert field.of_type == MyType + + +def test_list_with_string_type(): + field = List("graphene.types.tests.utils.MyLazyType") + assert field.of_type == MyLazyType + + def test_list_inherited_works_list(): _list = List(List(String)) assert isinstance(_list.of_type, List) @@ -35,6 +54,23 @@ def test_nonnull(): assert str(nonnull) == 'String!' +def test_nonnull_with_lazy_type(): + MyType = object() + field = NonNull(lambda: MyType) + assert field.of_type == MyType + + +def test_nonnull_with_lazy_partial_type(): + MyType = object() + field = NonNull(partial(lambda: MyType)) + assert field.of_type == MyType + + +def test_nonnull_with_string_type(): + field = NonNull("graphene.types.tests.utils.MyLazyType") + assert field.of_type == MyLazyType + + def test_nonnull_inherited_works_list(): _list = NonNull(List(String)) assert isinstance(_list.of_type, List) diff --git a/graphene/types/tests/utils.py b/graphene/types/tests/utils.py new file mode 100644 index 00000000..83cf49e2 --- /dev/null +++ b/graphene/types/tests/utils.py @@ -0,0 +1 @@ +MyLazyType = object() diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 16a9b3ca..0f75c113 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -283,6 +283,4 @@ class TypeMap(GraphQLTypeMap): return GraphQLList(self.get_field_type(map, type.of_type)) if isinstance(type, NonNull): return GraphQLNonNull(self.get_field_type(map, type.of_type)) - if inspect.isfunction(type): - type = type() return map.get(type._meta.name) diff --git a/graphene/types/utils.py b/graphene/types/utils.py index e2603155..19cc06e7 100644 --- a/graphene/types/utils.py +++ b/graphene/types/utils.py @@ -1,5 +1,9 @@ +import inspect from collections import OrderedDict +from functools import partial +from six import string_types +from ..utils.module_loading import import_string from .mountedtype import MountedType from .unmountedtype import UnmountedType @@ -62,3 +66,11 @@ def yank_fields_from_attrs(attrs, _as=None, delete=True, sort=True): if sort: fields_with_names = sorted(fields_with_names, key=lambda f: f[1]) return OrderedDict(fields_with_names) + + +def get_type(_type): + if isinstance(_type, string_types): + return import_string(_type) + if inspect.isfunction(_type) or type(_type) is partial: + return _type() + return _type From aaf9e92d24485ef0ffdd5cb7e87091783f3026a9 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 6 Apr 2017 22:20:56 -0700 Subject: [PATCH 34/58] =?UTF-8?q?Updated=20version=20to=201.3.0=20?= =?UTF-8?q?=F0=9F=92=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index 909370bb..6915a237 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -10,7 +10,7 @@ except NameError: __SETUP__ = False -VERSION = (1, 2, 0, 'final', 0) +VERSION = (1, 3, 0, 'final', 0) __version__ = get_version(VERSION) From afed25a18015643749439288a9536235232dd493 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 7 Apr 2017 22:02:16 -0700 Subject: [PATCH 35/58] Improved enums comparison and getters. Fixed #444 --- docs/types/enums.rst | 28 +++++++++++++++++- graphene/types/enum.py | 14 +++++++++ graphene/types/tests/test_enum.py | 49 +++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/docs/types/enums.rst b/docs/types/enums.rst index f0fefd11..c2a184e4 100644 --- a/docs/types/enums.rst +++ b/docs/types/enums.rst @@ -59,7 +59,33 @@ Notes ----- ``graphene.Enum`` uses |enum.Enum|_ internally (or a backport if -that's not available) and can be used in the exact same way. +that's not available) and can be used in a similar way, with the exception of +member getters. + +In the Python ``Enum`` implementation you can access a member by initing the Enum. + +.. code:: python + + from enum import Enum + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + assert Color(1) == Color.RED + + +However, in Graphene ``Enum`` you need to call get to have the same effect: + +.. code:: python + + from graphene import Enum + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + assert Color.get(1) == Color.RED .. |enum.Enum| replace:: ``enum.Enum`` .. _enum.Enum: https://docs.python.org/3/library/enum.html diff --git a/graphene/types/enum.py b/graphene/types/enum.py index a6575cea..029e6991 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -13,6 +13,12 @@ except ImportError: from ..pyutils.enum import Enum as PyEnum +def eq_enum(self, other): + if isinstance(other, self.__class__): + return self is other + return self.value is other + + class EnumTypeMeta(type): def __new__(cls, name, bases, attrs): @@ -28,6 +34,7 @@ class EnumTypeMeta(type): enum=None, ) if not options.enum: + attrs['__eq__'] = eq_enum options.enum = PyEnum(cls.__name__, attrs) new_attrs = OrderedDict(attrs, _meta=options, **options.enum.__members__) @@ -36,11 +43,18 @@ class EnumTypeMeta(type): def __prepare__(name, bases, **kwargs): # noqa: N805 return OrderedDict() + def get(cls, value): + return cls._meta.enum(value) + + def __getitem__(cls, value): + return cls._meta.enum[value] + def __call__(cls, *args, **kwargs): # noqa: N805 if cls is Enum: description = kwargs.pop('description', None) return cls.from_enum(PyEnum(*args, **kwargs), description=description) return super(EnumTypeMeta, cls).__call__(*args, **kwargs) + # return cls._meta.enum(*args, **kwargs) def from_enum(cls, enum, description=None): # noqa: N805 meta_class = type('Meta', (object,), {'enum': enum, 'description': description}) diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index a5a2d4c1..6cd22bd9 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -111,3 +111,52 @@ def test_enum_value_as_unmounted_argument(): unmounted_field = unmounted.Argument() assert isinstance(unmounted_field, Argument) assert unmounted_field.type == RGB + + +def test_enum_can_be_compared(): + class RGB(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + assert RGB.RED == 1 + assert RGB.GREEN == 2 + assert RGB.BLUE == 3 + + +def test_enum_can_be_initialzied(): + class RGB(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + assert RGB.get(1) == RGB.RED + assert RGB.get(2) == RGB.GREEN + assert RGB.get(3) == RGB.BLUE + + +def test_enum_can_retrieve_members(): + class RGB(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + assert RGB['RED'] == RGB.RED + assert RGB['GREEN'] == RGB.GREEN + assert RGB['BLUE'] == RGB.BLUE + + +def test_enum_to_enum_comparison_should_differ(): + class RGB1(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + class RGB2(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + assert RGB1.RED != RGB2.RED + assert RGB1.GREEN != RGB2.GREEN + assert RGB1.BLUE != RGB2.BLUE From dfcd7f256340301d7551e2af383e4e84454db097 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sun, 9 Apr 2017 21:04:03 -0700 Subject: [PATCH 36/58] =?UTF-8?q?First=20version=20of=20the=20Graphene=20t?= =?UTF-8?q?est=20client=20and=20snapshots=20=F0=9F=92=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/starwars/tests/snapshots/__init__.py | 0 .../tests/snapshots/snap_test_query.py | 205 ++++++++++++++++ examples/starwars/tests/test_query.py | 219 +++--------------- .../tests/snapshots/__init__.py | 0 .../tests/snapshots/snap_test_connections.py | 35 +++ .../tests/snapshots/snap_test_mutation.py | 65 ++++++ .../snap_test_objectidentification.py | 56 +++++ .../starwars_relay/tests/test_connections.py | 30 +-- .../starwars_relay/tests/test_mutation.py | 54 +---- .../tests/test_objectidentification.py | 63 ++--- graphene/test/__init__.py | 28 +++ setup.py | 1 + 12 files changed, 442 insertions(+), 314 deletions(-) create mode 100644 examples/starwars/tests/snapshots/__init__.py create mode 100644 examples/starwars/tests/snapshots/snap_test_query.py create mode 100644 examples/starwars_relay/tests/snapshots/__init__.py create mode 100644 examples/starwars_relay/tests/snapshots/snap_test_connections.py create mode 100644 examples/starwars_relay/tests/snapshots/snap_test_mutation.py create mode 100644 examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py create mode 100644 graphene/test/__init__.py diff --git a/examples/starwars/tests/snapshots/__init__.py b/examples/starwars/tests/snapshots/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/starwars/tests/snapshots/snap_test_query.py b/examples/starwars/tests/snapshots/snap_test_query.py new file mode 100644 index 00000000..63c097d4 --- /dev/null +++ b/examples/starwars/tests/snapshots/snap_test_query.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- + +# snapshottest: v1 +# https://pypi.python.org/pypi/snapshottest + +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_hero_name_query 1'] = { + 'data': { + 'hero': { + 'name': 'R2-D2' + } + } +} + +snapshots['test_hero_name_and_friends_query 1'] = { + 'data': { + 'hero': { + 'id': '2001', + 'name': 'R2-D2', + 'friends': [ + { + 'name': 'Luke Skywalker' + }, + { + 'name': 'Han Solo' + }, + { + 'name': 'Leia Organa' + } + ] + } + } +} + +snapshots['test_nested_query 1'] = { + 'data': { + 'hero': { + 'name': 'R2-D2', + 'friends': [ + { + 'name': 'Luke Skywalker', + 'appearsIn': [ + 'NEWHOPE', + 'EMPIRE', + 'JEDI' + ], + 'friends': [ + { + 'name': 'Han Solo' + }, + { + 'name': 'Leia Organa' + }, + { + 'name': 'C-3PO' + }, + { + 'name': 'R2-D2' + } + ] + }, + { + 'name': 'Han Solo', + 'appearsIn': [ + 'NEWHOPE', + 'EMPIRE', + 'JEDI' + ], + 'friends': [ + { + 'name': 'Luke Skywalker' + }, + { + 'name': 'Leia Organa' + }, + { + 'name': 'R2-D2' + } + ] + }, + { + 'name': 'Leia Organa', + 'appearsIn': [ + 'NEWHOPE', + 'EMPIRE', + 'JEDI' + ], + 'friends': [ + { + 'name': 'Luke Skywalker' + }, + { + 'name': 'Han Solo' + }, + { + 'name': 'C-3PO' + }, + { + 'name': 'R2-D2' + } + ] + } + ] + } + } +} + +snapshots['test_fetch_luke_query 1'] = { + 'data': { + 'human': { + 'name': 'Luke Skywalker' + } + } +} + +snapshots['test_fetch_some_id_query 1'] = { + 'data': { + 'human': { + 'name': 'Luke Skywalker' + } + } +} + +snapshots['test_fetch_some_id_query2 1'] = { + 'data': { + 'human': { + 'name': 'Han Solo' + } + } +} + +snapshots['test_invalid_id_query 1'] = { + 'data': { + 'human': None + } +} + +snapshots['test_fetch_luke_aliased 1'] = { + 'data': { + 'luke': { + 'name': 'Luke Skywalker' + } + } +} + +snapshots['test_fetch_luke_and_leia_aliased 1'] = { + 'data': { + 'luke': { + 'name': 'Luke Skywalker' + }, + 'leia': { + 'name': 'Leia Organa' + } + } +} + +snapshots['test_duplicate_fields 1'] = { + 'data': { + 'luke': { + 'name': 'Luke Skywalker', + 'homePlanet': 'Tatooine' + }, + 'leia': { + 'name': 'Leia Organa', + 'homePlanet': 'Alderaan' + } + } +} + +snapshots['test_use_fragment 1'] = { + 'data': { + 'luke': { + 'name': 'Luke Skywalker', + 'homePlanet': 'Tatooine' + }, + 'leia': { + 'name': 'Leia Organa', + 'homePlanet': 'Alderaan' + } + } +} + +snapshots['test_check_type_of_r2 1'] = { + 'data': { + 'hero': { + '__typename': 'Droid', + 'name': 'R2-D2' + } + } +} + +snapshots['test_check_type_of_luke 1'] = { + 'data': { + 'hero': { + '__typename': 'Human', + 'name': 'Luke Skywalker' + } + } +} diff --git a/examples/starwars/tests/test_query.py b/examples/starwars/tests/test_query.py index ac57c664..e6a70735 100644 --- a/examples/starwars/tests/test_query.py +++ b/examples/starwars/tests/test_query.py @@ -1,11 +1,12 @@ - +from graphene.test import Client from ..data import setup from ..schema import schema setup() +client = Client(schema) -def test_hero_name_query(): +def test_hero_name_query(snapshot): query = ''' query HeroNameQuery { hero { @@ -13,17 +14,11 @@ def test_hero_name_query(): } } ''' - expected = { - 'hero': { - 'name': 'R2-D2' - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) + -def test_hero_name_and_friends_query(): +def test_hero_name_and_friends_query(snapshot): query = ''' query HeroNameAndFriendsQuery { hero { @@ -35,23 +30,10 @@ def test_hero_name_and_friends_query(): } } ''' - expected = { - 'hero': { - 'id': '2001', - 'name': 'R2-D2', - 'friends': [ - {'name': 'Luke Skywalker'}, - {'name': 'Han Solo'}, - {'name': 'Leia Organa'}, - ] - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) -def test_nested_query(): +def test_nested_query(snapshot): query = ''' query NestedQuery { hero { @@ -66,70 +48,10 @@ def test_nested_query(): } } ''' - expected = { - 'hero': { - 'name': 'R2-D2', - 'friends': [ - { - 'name': 'Luke Skywalker', - 'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'], - 'friends': [ - { - 'name': 'Han Solo', - }, - { - 'name': 'Leia Organa', - }, - { - 'name': 'C-3PO', - }, - { - 'name': 'R2-D2', - }, - ] - }, - { - 'name': 'Han Solo', - 'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'], - 'friends': [ - { - 'name': 'Luke Skywalker', - }, - { - 'name': 'Leia Organa', - }, - { - 'name': 'R2-D2', - }, - ] - }, - { - 'name': 'Leia Organa', - 'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'], - 'friends': [ - { - 'name': 'Luke Skywalker', - }, - { - 'name': 'Han Solo', - }, - { - 'name': 'C-3PO', - }, - { - 'name': 'R2-D2', - }, - ] - }, - ] - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) -def test_fetch_luke_query(): +def test_fetch_luke_query(snapshot): query = ''' query FetchLukeQuery { human(id: "1000") { @@ -137,17 +59,10 @@ def test_fetch_luke_query(): } } ''' - expected = { - 'human': { - 'name': 'Luke Skywalker', - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) -def test_fetch_some_id_query(): +def test_fetch_some_id_query(snapshot): query = ''' query FetchSomeIDQuery($someId: String!) { human(id: $someId) { @@ -158,17 +73,10 @@ def test_fetch_some_id_query(): params = { 'someId': '1000', } - expected = { - 'human': { - 'name': 'Luke Skywalker', - } - } - result = schema.execute(query, None, variable_values=params) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query, variable_values=params)) -def test_fetch_some_id_query2(): +def test_fetch_some_id_query2(snapshot): query = ''' query FetchSomeIDQuery($someId: String!) { human(id: $someId) { @@ -179,17 +87,10 @@ def test_fetch_some_id_query2(): params = { 'someId': '1002', } - expected = { - 'human': { - 'name': 'Han Solo', - } - } - result = schema.execute(query, None, variable_values=params) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query, variable_values=params)) -def test_invalid_id_query(): +def test_invalid_id_query(snapshot): query = ''' query humanQuery($id: String!) { human(id: $id) { @@ -200,15 +101,10 @@ def test_invalid_id_query(): params = { 'id': 'not a valid id', } - expected = { - 'human': None - } - result = schema.execute(query, None, variable_values=params) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query, variable_values=params)) -def test_fetch_luke_aliased(): +def test_fetch_luke_aliased(snapshot): query = ''' query FetchLukeAliased { luke: human(id: "1000") { @@ -216,17 +112,10 @@ def test_fetch_luke_aliased(): } } ''' - expected = { - 'luke': { - 'name': 'Luke Skywalker', - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) -def test_fetch_luke_and_leia_aliased(): +def test_fetch_luke_and_leia_aliased(snapshot): query = ''' query FetchLukeAndLeiaAliased { luke: human(id: "1000") { @@ -237,20 +126,10 @@ def test_fetch_luke_and_leia_aliased(): } } ''' - expected = { - 'luke': { - 'name': 'Luke Skywalker', - }, - 'leia': { - 'name': 'Leia Organa', - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) -def test_duplicate_fields(): +def test_duplicate_fields(snapshot): query = ''' query DuplicateFields { luke: human(id: "1000") { @@ -263,22 +142,10 @@ def test_duplicate_fields(): } } ''' - expected = { - 'luke': { - 'name': 'Luke Skywalker', - 'homePlanet': 'Tatooine', - }, - 'leia': { - 'name': 'Leia Organa', - 'homePlanet': 'Alderaan', - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) -def test_use_fragment(): +def test_use_fragment(snapshot): query = ''' query UseFragment { luke: human(id: "1000") { @@ -293,22 +160,10 @@ def test_use_fragment(): homePlanet } ''' - expected = { - 'luke': { - 'name': 'Luke Skywalker', - 'homePlanet': 'Tatooine', - }, - 'leia': { - 'name': 'Leia Organa', - 'homePlanet': 'Alderaan', - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) -def test_check_type_of_r2(): +def test_check_type_of_r2(snapshot): query = ''' query CheckTypeOfR2 { hero { @@ -317,18 +172,10 @@ def test_check_type_of_r2(): } } ''' - expected = { - 'hero': { - '__typename': 'Droid', - 'name': 'R2-D2', - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) -def test_check_type_of_luke(): +def test_check_type_of_luke(snapshot): query = ''' query CheckTypeOfLuke { hero(episode: EMPIRE) { @@ -337,12 +184,4 @@ def test_check_type_of_luke(): } } ''' - expected = { - 'hero': { - '__typename': 'Human', - 'name': 'Luke Skywalker', - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) diff --git a/examples/starwars_relay/tests/snapshots/__init__.py b/examples/starwars_relay/tests/snapshots/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/starwars_relay/tests/snapshots/snap_test_connections.py b/examples/starwars_relay/tests/snapshots/snap_test_connections.py new file mode 100644 index 00000000..b0efcc97 --- /dev/null +++ b/examples/starwars_relay/tests/snapshots/snap_test_connections.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +# snapshottest: v1 +# https://pypi.python.org/pypi/snapshottest + +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_correct_fetch_first_ship_rebels 1'] = { + 'data': { + 'rebels': { + 'name': 'Alliance to Restore the Republic', + 'ships': { + 'pageInfo': { + 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', + 'endCursor': 'YXJyYXljb25uZWN0aW9uOjA=', + 'hasNextPage': True, + 'hasPreviousPage': False + }, + 'edges': [ + { + 'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', + 'node': { + 'name': 'X-Wing' + } + } + ] + } + } + } +} diff --git a/examples/starwars_relay/tests/snapshots/snap_test_mutation.py b/examples/starwars_relay/tests/snapshots/snap_test_mutation.py new file mode 100644 index 00000000..95f414cc --- /dev/null +++ b/examples/starwars_relay/tests/snapshots/snap_test_mutation.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +# snapshottest: v1 +# https://pypi.python.org/pypi/snapshottest + +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_mutations 1'] = { + 'data': { + 'introduceShip': { + 'ship': { + 'id': 'U2hpcDo5', + 'name': 'Peter' + }, + 'faction': { + 'name': 'Alliance to Restore the Republic', + 'ships': { + 'edges': [ + { + 'node': { + 'id': 'U2hpcDox', + 'name': 'X-Wing' + } + }, + { + 'node': { + 'id': 'U2hpcDoy', + 'name': 'Y-Wing' + } + }, + { + 'node': { + 'id': 'U2hpcDoz', + 'name': 'A-Wing' + } + }, + { + 'node': { + 'id': 'U2hpcDo0', + 'name': 'Millenium Falcon' + } + }, + { + 'node': { + 'id': 'U2hpcDo1', + 'name': 'Home One' + } + }, + { + 'node': { + 'id': 'U2hpcDo5', + 'name': 'Peter' + } + } + ] + } + } + } + } +} diff --git a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py new file mode 100644 index 00000000..07e20039 --- /dev/null +++ b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +# snapshottest: v1 +# https://pypi.python.org/pypi/snapshottest + +from __future__ import unicode_literals + +from snapshottest import Snapshot + + +snapshots = Snapshot() + +snapshots['test_correctly_fetches_id_name_rebels 1'] = { + 'data': { + 'rebels': { + 'id': 'RmFjdGlvbjox', + 'name': 'Alliance to Restore the Republic' + } + } +} + +snapshots['test_correctly_refetches_rebels 1'] = { + 'data': { + 'node': { + 'id': 'RmFjdGlvbjox', + 'name': 'Alliance to Restore the Republic' + } + } +} + +snapshots['test_correctly_fetches_id_name_empire 1'] = { + 'data': { + 'empire': { + 'id': 'RmFjdGlvbjoy', + 'name': 'Galactic Empire' + } + } +} + +snapshots['test_correctly_refetches_empire 1'] = { + 'data': { + 'node': { + 'id': 'RmFjdGlvbjoy', + 'name': 'Galactic Empire' + } + } +} + +snapshots['test_correctly_refetches_xwing 1'] = { + 'data': { + 'node': { + 'id': 'U2hpcDox', + 'name': 'X-Wing' + } + } +} diff --git a/examples/starwars_relay/tests/test_connections.py b/examples/starwars_relay/tests/test_connections.py index d77122b9..e3ecfa7b 100644 --- a/examples/starwars_relay/tests/test_connections.py +++ b/examples/starwars_relay/tests/test_connections.py @@ -1,10 +1,13 @@ +from graphene.test import Client from ..data import setup from ..schema import schema setup() +client = Client(schema) -def test_correct_fetch_first_ship_rebels(): + +def test_correct_fetch_first_ship_rebels(snapshot): query = ''' query RebelsShipsQuery { rebels { @@ -26,27 +29,4 @@ def test_correct_fetch_first_ship_rebels(): } } ''' - expected = { - 'rebels': { - 'name': 'Alliance to Restore the Republic', - 'ships': { - 'pageInfo': { - 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', - 'endCursor': 'YXJyYXljb25uZWN0aW9uOjA=', - 'hasNextPage': True, - 'hasPreviousPage': False - }, - 'edges': [ - { - 'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', - 'node': { - 'name': 'X-Wing' - } - } - ] - } - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) diff --git a/examples/starwars_relay/tests/test_mutation.py b/examples/starwars_relay/tests/test_mutation.py index e5ae2716..2c07f08c 100644 --- a/examples/starwars_relay/tests/test_mutation.py +++ b/examples/starwars_relay/tests/test_mutation.py @@ -1,10 +1,13 @@ +from graphene.test import Client from ..data import setup from ..schema import schema setup() +client = Client(schema) -def test_mutations(): + +def test_mutations(snapshot): query = ''' mutation MyMutation { introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) { @@ -26,51 +29,4 @@ def test_mutations(): } } ''' - expected = { - 'introduceShip': { - 'ship': { - 'id': 'U2hpcDo5', - 'name': 'Peter' - }, - 'faction': { - 'name': 'Alliance to Restore the Republic', - 'ships': { - 'edges': [{ - 'node': { - 'id': 'U2hpcDox', - 'name': 'X-Wing' - } - }, { - 'node': { - 'id': 'U2hpcDoy', - 'name': 'Y-Wing' - } - }, { - 'node': { - 'id': 'U2hpcDoz', - 'name': 'A-Wing' - } - }, { - 'node': { - 'id': 'U2hpcDo0', - 'name': 'Millenium Falcon' - } - }, { - 'node': { - 'id': 'U2hpcDo1', - 'name': 'Home One' - } - }, { - 'node': { - 'id': 'U2hpcDo5', - 'name': 'Peter' - } - }] - }, - } - } - } - result = schema.execute(query) - # raise result.errors[0].original_error, None, result.errors[0].stack - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) diff --git a/examples/starwars_relay/tests/test_objectidentification.py b/examples/starwars_relay/tests/test_objectidentification.py index 327d5b0c..d1b4c529 100644 --- a/examples/starwars_relay/tests/test_objectidentification.py +++ b/examples/starwars_relay/tests/test_objectidentification.py @@ -1,8 +1,11 @@ +from graphene.test import Client from ..data import setup from ..schema import schema setup() +client = Client(schema) + def test_str_schema(): assert str(schema) == '''schema { @@ -66,7 +69,7 @@ type ShipEdge { ''' -def test_correctly_fetches_id_name_rebels(): +def test_correctly_fetches_id_name_rebels(snapshot): query = ''' query RebelsQuery { rebels { @@ -75,18 +78,10 @@ def test_correctly_fetches_id_name_rebels(): } } ''' - expected = { - 'rebels': { - 'id': 'RmFjdGlvbjox', - 'name': 'Alliance to Restore the Republic' - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) -def test_correctly_refetches_rebels(): +def test_correctly_refetches_rebels(snapshot): query = ''' query RebelsRefetchQuery { node(id: "RmFjdGlvbjox") { @@ -97,18 +92,10 @@ def test_correctly_refetches_rebels(): } } ''' - expected = { - 'node': { - 'id': 'RmFjdGlvbjox', - 'name': 'Alliance to Restore the Republic' - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) -def test_correctly_fetches_id_name_empire(): +def test_correctly_fetches_id_name_empire(snapshot): query = ''' query EmpireQuery { empire { @@ -117,18 +104,10 @@ def test_correctly_fetches_id_name_empire(): } } ''' - expected = { - 'empire': { - 'id': 'RmFjdGlvbjoy', - 'name': 'Galactic Empire' - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) -def test_correctly_refetches_empire(): +def test_correctly_refetches_empire(snapshot): query = ''' query EmpireRefetchQuery { node(id: "RmFjdGlvbjoy") { @@ -139,18 +118,10 @@ def test_correctly_refetches_empire(): } } ''' - expected = { - 'node': { - 'id': 'RmFjdGlvbjoy', - 'name': 'Galactic Empire' - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) -def test_correctly_refetches_xwing(): +def test_correctly_refetches_xwing(snapshot): query = ''' query XWingRefetchQuery { node(id: "U2hpcDox") { @@ -161,12 +132,4 @@ def test_correctly_refetches_xwing(): } } ''' - expected = { - 'node': { - 'id': 'U2hpcDox', - 'name': 'X-Wing' - } - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected + snapshot.assert_match(client.execute(query)) diff --git a/graphene/test/__init__.py b/graphene/test/__init__.py new file mode 100644 index 00000000..fccc80ce --- /dev/null +++ b/graphene/test/__init__.py @@ -0,0 +1,28 @@ +from graphene.types.schema import Schema + +from graphql.error import format_error + + +def format_execution_result(execution_result): + if execution_result: + response = {} + + if execution_result.errors: + response['errors'] = [format_error(e) for e in execution_result.errors] + + if not execution_result.invalid: + response['data'] = execution_result.data + + return response + + +class Client(object): + def __init__(self, schema, **execute_options): + assert isinstance(schema, Schema) + self.schema = schema + self.execute_options = execute_options + + def execute(self, *args, **kwargs): + return format_execution_result( + self.schema.execute(*args, **dict(self.execute_options, **kwargs)) + ) diff --git a/setup.py b/setup.py index 20742eef..215a97df 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ tests_require = [ 'pytest>=2.7.2', 'pytest-benchmark', 'pytest-cov', + 'snapshottest', 'coveralls', 'six', 'mock', From 60e29028a8c7e025d5bbfe1d9f730c937829c98c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 10 Apr 2017 00:47:20 -0700 Subject: [PATCH 37/58] Added testing docs --- docs/index.rst | 1 + docs/testing/index.rst | 90 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 docs/testing/index.rst diff --git a/docs/index.rst b/docs/index.rst index 675051b3..f1761937 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,6 +10,7 @@ Contents: types/index execution/index relay/index + testing/index Integrations ----- diff --git a/docs/testing/index.rst b/docs/testing/index.rst new file mode 100644 index 00000000..0dcb0034 --- /dev/null +++ b/docs/testing/index.rst @@ -0,0 +1,90 @@ +=================== +Testing in Graphene +=================== + + +Automated testing is an extremely useful bug-killing tool for the modern developer. You can use a collection of tests – a test suite – to solve, or avoid, a number of problems: + +- When you’re writing new code, you can use tests to validate your code works as expected. +- When you’re refactoring or modifying old code, you can use tests to ensure your changes haven’t affected your application’s behavior unexpectedly. + +Testing a GraphQL application is a complex task, because a GraphQL application is made of several layers of logic – schema definition, schema validation, permissions and field resolution. + +With Graphene test-execution framework and assorted utilities, you can simulate GraphQL requests, execute mutations, inspect your application’s output and generally verify your code is doing what it should be doing. + + +Testing tools +------------- + +Graphene provides a small set of tools that come in handy when writing tests. + + +Test Client +~~~~~~~~~~~ + +The test client is a Python class that acts as a dummy GraphQL client, allowing you to test your views and interact with your Graphene-powered application programmatically. + +Some of the things you can do with the test client are: + +- Simulate Queries and Mutations and observe the response. +- Test that a given query request is rendered by a given Django template, with a template context that contains certain values. + + +Overview and a quick example +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To use the test client, instantiate ``graphene.test.Client`` and retrieve GraphQL responses: + + +.. code:: python + + from graphene.test import Client + + def test_hey(): + client = Client(my_schema) + executed = client.execute('''{ hey }''') + assert executed == { + 'data': { + 'hey': 'hello!' + } + } + + +Snapshot testing +~~~~~~~~~~~~~~~~ + +As our APIs evolve, we need to know when our changes introduce any breaking changes that might break +some of the clients of our GraphQL app. + +However, writing tests and replicate the same response we expect from our GraphQL application can be +tedious and repetitive task, and sometimes it's easier to skip this process. + +Because of that, we recommend the usage of `SnapshotTest `_. + +SnapshotTest let us write all this tests in a breeze, as creates automatically the ``snapshots`` for us +the first time the test is executed. + + +Here is a simple example on how our tests will look if we use ``pytest``: + +.. code:: python + + def test_hey(snapshot): + client = Client(my_schema) + # This will create a snapshot dir and a snapshot file + # the first time the test is executed, with the response + # of the execution. + snapshot.assert_match(client.execute('''{ hey }''')) + + +If we are using ``unittest``: + +.. code:: python + + from snapshottest import TestCase + + class APITestCase(TestCase): + def test_api_me(self): + """Testing the API for /me""" + client = Client(my_schema) + self.assertMatchSnapshot(client.execute('''{ hey }''')) From b369ccfa0828817f41b647938d926f5c2a4cb192 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 10 Apr 2017 04:26:54 -0700 Subject: [PATCH 38/58] Updated snapshot files --- examples/starwars/tests/snapshots/snap_test_query.py | 5 +---- .../starwars_relay/tests/snapshots/snap_test_connections.py | 5 +---- .../starwars_relay/tests/snapshots/snap_test_mutation.py | 5 +---- .../tests/snapshots/snap_test_objectidentification.py | 5 +---- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/examples/starwars/tests/snapshots/snap_test_query.py b/examples/starwars/tests/snapshots/snap_test_query.py index 63c097d4..9314420b 100644 --- a/examples/starwars/tests/snapshots/snap_test_query.py +++ b/examples/starwars/tests/snapshots/snap_test_query.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- - -# snapshottest: v1 -# https://pypi.python.org/pypi/snapshottest - +# snapshottest: v1 - https://goo.gl/zC4yUc from __future__ import unicode_literals from snapshottest import Snapshot diff --git a/examples/starwars_relay/tests/snapshots/snap_test_connections.py b/examples/starwars_relay/tests/snapshots/snap_test_connections.py index b0efcc97..cbf14d95 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_connections.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_connections.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- - -# snapshottest: v1 -# https://pypi.python.org/pypi/snapshottest - +# snapshottest: v1 - https://goo.gl/zC4yUc from __future__ import unicode_literals from snapshottest import Snapshot diff --git a/examples/starwars_relay/tests/snapshots/snap_test_mutation.py b/examples/starwars_relay/tests/snapshots/snap_test_mutation.py index 95f414cc..27c3f9bb 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_mutation.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_mutation.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- - -# snapshottest: v1 -# https://pypi.python.org/pypi/snapshottest - +# snapshottest: v1 - https://goo.gl/zC4yUc from __future__ import unicode_literals from snapshottest import Snapshot diff --git a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py index 07e20039..a6095bb0 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- - -# snapshottest: v1 -# https://pypi.python.org/pypi/snapshottest - +# snapshottest: v1 - https://goo.gl/zC4yUc from __future__ import unicode_literals from snapshottest import Snapshot From 917dc16ea64b3d896e81f5648dea0aebb78b468e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 10 Apr 2017 16:10:35 -0700 Subject: [PATCH 39/58] Fixed format_error --- graphene/test/__init__.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/graphene/test/__init__.py b/graphene/test/__init__.py index fccc80ce..6c47fdeb 100644 --- a/graphene/test/__init__.py +++ b/graphene/test/__init__.py @@ -1,9 +1,17 @@ from graphene.types.schema import Schema -from graphql.error import format_error +from graphql.error import format_error as format_graphql_error +from graphql.error import GraphQLError -def format_execution_result(execution_result): +def default_format_error(error): + if isinstance(error, GraphQLError): + return format_graphql_error(error) + + return {'message': six.text_type(error)} + + +def format_execution_result(execution_result, format_error): if execution_result: response = {} @@ -17,12 +25,14 @@ def format_execution_result(execution_result): class Client(object): - def __init__(self, schema, **execute_options): + def __init__(self, schema, format_error=None, **execute_options): assert isinstance(schema, Schema) self.schema = schema self.execute_options = execute_options + self.format_error = format_error or default_format_error def execute(self, *args, **kwargs): return format_execution_result( - self.schema.execute(*args, **dict(self.execute_options, **kwargs)) + self.schema.execute(*args, **dict(self.execute_options, **kwargs)), + self.format_error ) From 038f81e8e1931e1f5fb55cad80773bf6c2d2d09c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 10 Apr 2017 16:18:45 -0700 Subject: [PATCH 40/58] Added extra parameters section in testing docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @BossGrand 😉 --- docs/testing/index.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/testing/index.rst b/docs/testing/index.rst index 0dcb0034..516f0e8d 100644 --- a/docs/testing/index.rst +++ b/docs/testing/index.rst @@ -50,6 +50,27 @@ To use the test client, instantiate ``graphene.test.Client`` and retrieve GraphQ } +Execute parameters +~~~~~~~~~~~~~~~~~~ + +You can also add extra keyword arguments to the ``execute`` method, such as +``context_value``, ``root_value``, ``variable_values``, ...: + + +.. code:: python + + from graphene.test import Client + + def test_hey(): + client = Client(my_schema) + executed = client.execute('''{ hey }''', context_value={'user': 'Peter'}) + assert executed == { + 'data': { + 'hey': 'hello Peter!' + } + } + + Snapshot testing ~~~~~~~~~~~~~~~~ From 6c040e68a2474ec17f1d2ccd5a4b295ffab63dbd Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 10 Apr 2017 16:18:52 -0700 Subject: [PATCH 41/58] Fixed six import --- graphene/test/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphene/test/__init__.py b/graphene/test/__init__.py index 6c47fdeb..98c8547c 100644 --- a/graphene/test/__init__.py +++ b/graphene/test/__init__.py @@ -1,8 +1,9 @@ -from graphene.types.schema import Schema - +import six from graphql.error import format_error as format_graphql_error from graphql.error import GraphQLError +from graphene.types.schema import Schema + def default_format_error(error): if isinstance(error, GraphQLError): From e92b03bed5e1a12c558d5b1c8a03b92eecff3218 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 12 Apr 2017 23:23:44 -0700 Subject: [PATCH 42/58] Allow Node inner types to be lazy. Fixed #437 --- graphene/relay/node.py | 14 ++++++++------ graphene/relay/tests/test_node.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index cf395058..aa6e2dc0 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -5,6 +5,7 @@ import six from graphql_relay import from_global_id, to_global_id from ..types import ID, Field, Interface, ObjectType +from ..types.utils import get_type from ..types.interface import InterfaceMeta @@ -64,17 +65,18 @@ class NodeField(Field): name=None, **kwargs): assert issubclass(node, Node), 'NodeField can only operate in Nodes' self.node_type = node - - # If we don's specify a type, the field type will be the node interface - field_type = type or node + self.field_type = type super(NodeField, self).__init__( - field_type, + # If we don's specify a type, the field type will be the node interface + type or node, description='The ID of the object', - id=ID(required=True), - resolver=partial(node.node_resolver, only_type=type) + id=ID(required=True) ) + def get_resolver(self, parent_resolver): + return partial(self.node_type.node_resolver, only_type=get_type(self.field_type)) + class Node(six.with_metaclass(NodeMeta, Interface)): '''An object with an ID''' diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index a72c512f..6a9c2e04 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -45,6 +45,7 @@ class RootQuery(ObjectType): first = String() node = Node.Field() only_node = Node.Field(MyNode) + only_node_lazy = Node.Field(lambda: MyNode) schema = Schema(query=RootQuery, types=[MyNode, MyOtherNode]) @@ -116,6 +117,23 @@ def test_node_field_only_type_wrong(): assert executed.data == { 'onlyNode': None } +def test_node_field_only_lazy_type(): + executed = schema.execute( + '{ onlyNodeLazy(id:"%s") { __typename, name } } ' % Node.to_global_id("MyNode", 1) + ) + assert not executed.errors + assert executed.data == {'onlyNodeLazy': {'__typename': 'MyNode', 'name': '1'}} + + +def test_node_field_only_lazy_type_wrong(): + executed = schema.execute( + '{ onlyNodeLazy(id:"%s") { __typename, name } } ' % Node.to_global_id("MyOtherNode", 1) + ) + assert len(executed.errors) == 1 + assert str(executed.errors[0]) == 'Must receive an MyOtherNode id.' + assert executed.data == { 'onlyNodeLazy': None } + + def test_str_schema(): assert str(schema) == """ schema { @@ -142,5 +160,6 @@ type RootQuery { first: String node(id: ID!): Node onlyNode(id: ID!): MyNode + onlyNodeLazy(id: ID!): MyNode } """.lstrip() From 06757f10c6cbfb3885531b02a2c93e94491380a5 Mon Sep 17 00:00:00 2001 From: Kuan Date: Thu, 13 Apr 2017 10:32:49 -0700 Subject: [PATCH 43/58] Update basic schema with arguments --- docs/quickstart.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 09dddb6f..44a686cf 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -30,17 +30,17 @@ server with an associated set of resolve methods that know how to fetch data. We are going to create a very simple schema, with a ``Query`` with only -one field: ``hello``. And when we query it, it should return ``"World"``. +one field: ``hello`` and an input name. And when we query it, it should return ``"Hello {name}"``. .. code:: python import graphene class Query(graphene.ObjectType): - hello = graphene.String() + hello = graphene.String(name=graphene.Argument(graphene.String, default_value="stranger")) def resolve_hello(self, args, context, info): - return 'World' + return 'Hello ' + args['name'] schema = graphene.Schema(query=Query) @@ -52,6 +52,6 @@ Then we can start querying our schema: .. code:: python result = schema.execute('{ hello }') - print result.data['hello'] # "World" + print result.data['hello'] # "Hello stranger" Congrats! You got your first graphene schema working! From a7511d3a2c2eb59ad8342d3db85f18ea477ae379 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 14 Apr 2017 22:32:12 -0700 Subject: [PATCH 44/58] Added possible_types option to ObjectType.Meta --- graphene/types/objecttype.py | 6 ++++++ graphene/types/tests/test_objecttype.py | 24 ++++++++++++++++++++++++ graphene/types/tests/test_typemap.py | 15 +++++++++++++++ graphene/types/typemap.py | 11 ++++++++++- 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index bedc5e76..1f0e5b8d 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -24,6 +24,7 @@ class ObjectTypeMeta(AbstractTypeMeta): name=name, description=trim_docstring(attrs.get('__doc__')), interfaces=(), + possible_types=(), default_resolver=None, local_fields=OrderedDict(), ) @@ -55,6 +56,11 @@ class ObjectTypeMeta(AbstractTypeMeta): cls = type.__new__(cls, name, bases, dict(attrs, _meta=options)) + assert not (options.possible_types and cls.is_type_of), ( + '{}.Meta.possible_types will cause type collision with {}.is_type_of. ' + 'Please use one or other.' + ).format(name, name) + for interface in options.interfaces: interface.implements(cls) diff --git a/graphene/types/tests/test_objecttype.py b/graphene/types/tests/test_objecttype.py index 8ccc0fac..8a1cf898 100644 --- a/graphene/types/tests/test_objecttype.py +++ b/graphene/types/tests/test_objecttype.py @@ -184,3 +184,27 @@ def test_generate_objecttype_description(): ''' assert MyObjectType._meta.description == "Documentation\n\nDocumentation line 2" + + +def test_objecttype_with_possible_types(): + class MyObjectType(ObjectType): + class Meta: + possible_types = (dict, ) + + assert MyObjectType._meta.possible_types == (dict, ) + + +def test_objecttype_with_possible_types_and_is_type_of_should_raise(): + with pytest.raises(AssertionError) as excinfo: + class MyObjectType(ObjectType): + class Meta: + possible_types = (dict, ) + + @classmethod + def is_type_of(cls, root, context, info): + return False + + assert str(excinfo.value) == ( + 'MyObjectType.Meta.possible_types will cause type collision with ' + 'MyObjectType.is_type_of. Please use one or other.' + ) diff --git a/graphene/types/tests/test_typemap.py b/graphene/types/tests/test_typemap.py index 974b633e..475d0905 100644 --- a/graphene/types/tests/test_typemap.py +++ b/graphene/types/tests/test_typemap.py @@ -183,3 +183,18 @@ def test_objecttype_camelcase_disabled(): assert foo_field.args == { 'bar_foo': GraphQLArgument(GraphQLString, out_name='bar_foo') } + + +def test_objecttype_with_possible_types(): + class MyObjectType(ObjectType): + '''Description''' + class Meta: + possible_types = (dict, ) + + foo_bar = String() + + typemap = TypeMap([MyObjectType]) + graphql_type = typemap['MyObjectType'] + assert graphql_type.is_type_of + assert graphql_type.is_type_of({}, None, None) is True + assert graphql_type.is_type_of(MyObjectType(), None, None) is False diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 0f75c113..0b59afb5 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -52,6 +52,10 @@ def resolve_type(resolve_type_func, map, type_name, root, context, info): return _type +def is_type_of_from_possible_types(possible_types, root, context, info): + return isinstance(root, possible_types) + + class TypeMap(GraphQLTypeMap): def __init__(self, types, auto_camelcase=True, schema=None): @@ -153,12 +157,17 @@ class TypeMap(GraphQLTypeMap): interfaces.append(internal_type) return interfaces + if type._meta.possible_types: + is_type_of = partial(is_type_of_from_possible_types, type._meta.possible_types) + else: + is_type_of = type.is_type_of + return GrapheneObjectType( graphene_type=type, name=type._meta.name, description=type._meta.description, fields=partial(self.construct_fields_for_type, map, type), - is_type_of=type.is_type_of, + is_type_of=is_type_of, interfaces=interfaces ) From bd754c19892abdf60a6ef410cd94aeebb3d044ae Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 18 Apr 2017 22:55:29 -0700 Subject: [PATCH 45/58] Updated dependencies: use promise 2.0 and graphql-core 1.1 --- setup.py | 4 ++-- tox.ini | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 215a97df..3405e0c5 100644 --- a/setup.py +++ b/setup.py @@ -82,9 +82,9 @@ setup( install_requires=[ 'six>=1.10.0', - 'graphql-core>=1.0.1', + 'graphql-core>=1.1', 'graphql-relay>=0.4.5', - 'promise>=1.0.1', + 'promise>=2.0', ], tests_require=tests_require, extras_require={ diff --git a/tox.ini b/tox.ini index 13d40c96..48d4f5fc 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,8 @@ skipsdist = true [testenv] deps= pytest>=2.7.2 - graphql-core>=1.0.1 + graphql-core>=1.1 + promise>=2.0 graphql-relay>=0.4.5 six blinker From b71a2cb69e50c8f3bbc4d40d3ccda003b8088a3c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 20 Apr 2017 00:34:05 -0700 Subject: [PATCH 46/58] Fixed middleware docs to use whitespaces instead of tabs --- docs/execution/middleware.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst index 54eb55db..3303ed41 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -30,15 +30,15 @@ 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) + 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()]) + result = schema.execute('THE QUERY', middleware=[AuthorizationMiddleware()]) From 082186c16969b098fadf42645df35dde3d93f674 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 20 Apr 2017 00:53:17 -0700 Subject: [PATCH 47/58] Added dataloader docs --- docs/execution/dataloader.rst | 106 ++++++++++++++++++++++++++++++++++ docs/execution/execute.rst | 32 ++++++++++ docs/execution/index.rst | 36 +----------- 3 files changed, 141 insertions(+), 33 deletions(-) create mode 100644 docs/execution/dataloader.rst create mode 100644 docs/execution/execute.rst diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst new file mode 100644 index 00000000..4e6e60dc --- /dev/null +++ b/docs/execution/dataloader.rst @@ -0,0 +1,106 @@ +Dataloader +========== + +DataLoader is a generic utility to be used as part of your application's +data fetching layer to provide a simplified and consistent API over +various remote data sources such as databases or web services via batching +and caching. + + +Batching +-------- + +Batching is not an advanced feature, it's DataLoader's primary feature. +Create loaders by providing a batch loading function. + +.. code:: python + + from promise import Promise + from promise.dataloader import DataLoader + + class UserLoader(DataLoader): + def batch_load_fn(self, keys): + # Here we return a promise that will result on the + # corresponding user for each key in keys + return Promise.resolve([get_user(id=key) for key in keys]) + + +A batch loading function accepts an list of keys, and returns a ``Promise`` +which resolves to an list of ``values``. + +Then load individual values from the loader. ``DataLoader`` will coalesce all +individual loads which occur within a single frame of execution (executed once +the wrapping promise is resolved) and then call your batch function with all +requested keys. + + + +.. code:: python + + user_loader = UserLoader() + + user_loader.load(1).then(lambda user: user_loader.load(user.best_friend_id)) + + user_loader.load(2).then(lambda user: user_loader.load(user.best_friend_id)) + + +A naive application may have issued *four* round-trips to a backend for the +required information, but with ``DataLoader`` this application will make at most *two*. + +``DataLoader`` allows you to decouple unrelated parts of your application without +sacrificing the performance of batch data-loading. While the loader presents +an API that loads individual values, all concurrent requests will be coalesced +and presented to your batch loading function. This allows your application to +safely distribute data fetching requirements throughout your application and +maintain minimal outgoing data requests. + + + +Using with Graphene +------------------- + +DataLoader pairs nicely well with Grapehne/GraphQL. GraphQL fields are designed +to be stand-alone functions. Without a caching or batching mechanism, it's easy +for a naive GraphQL server to issue new database requests each time a field is resolved. + +Consider the following GraphQL request: + + +.. code:: + + { + me { + name + bestFriend { + name + } + friends(first: 5) { + name + bestFriend { + name + } + } + } + } + + +Naively, if ``me``, ``bestFriend`` and ``friends`` each need to request the backend, +there could be at most 13 database requests! + + +When using DataLoader, we could define the User type using our previous example with +learer code and at most 4 database requests, and possibly fewer if there are cache hits. + + +.. code:: python + + class User(graphene.ObjectType): + name = graphene.String() + best_friend = graphene.Field(lambda: User) + friends = graphene.List(lambda: User) + + def resolve_best_friend(self, args, context, info): + return user_loader.load(self.best_friend_id) + + def resolve_friends(self, args, context, info): + return user_loader.load_many(self.friend_ids) diff --git a/docs/execution/execute.rst b/docs/execution/execute.rst new file mode 100644 index 00000000..0e4de5ac --- /dev/null +++ b/docs/execution/execute.rst @@ -0,0 +1,32 @@ +Executing a query +================= + + +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'}) + diff --git a/docs/execution/index.rst b/docs/execution/index.rst index 849832d4..00d98ffb 100644 --- a/docs/execution/index.rst +++ b/docs/execution/index.rst @@ -2,39 +2,9 @@ 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 + :maxdepth: 2 + execute middleware + dataloader From 8cae7bd16f6d3bba7e933950e3128ca8e6b61f66 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 20 Apr 2017 00:55:13 -0700 Subject: [PATCH 48/58] Updated version to 1.4.0 --- graphene/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/__init__.py b/graphene/__init__.py index 6915a237..5019b935 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -10,7 +10,7 @@ except NameError: __SETUP__ = False -VERSION = (1, 3, 0, 'final', 0) +VERSION = (1, 4, 0, 'final', 0) __version__ = get_version(VERSION) From 5052536787503290fee145ada575ec8810fdf308 Mon Sep 17 00:00:00 2001 From: Michael Chen Date: Wed, 26 Apr 2017 11:41:51 -0700 Subject: [PATCH 49/58] Update Relay Documentation Link Update the link of Relay Specification to Facebook Relay Specifications --- docs/relay/nodes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/relay/nodes.rst b/docs/relay/nodes.rst index 4b2c96aa..5f470055 100644 --- a/docs/relay/nodes.rst +++ b/docs/relay/nodes.rst @@ -98,4 +98,5 @@ Example usage: # Should be CustomNode.Field() if we want to use our custom Node node = relay.Node.Field() +.. _Relay specification: https://facebook.github.io/relay/docs/graphql-relay-specification.html .. _Starwars Relay example: https://github.com/graphql-python/graphene/blob/master/examples/starwars_relay/schema.py From bad6f5a18873c5ef6488910b8f58728b8ff1ac1e Mon Sep 17 00:00:00 2001 From: Yixi Zhang Date: Wed, 26 Apr 2017 16:27:44 -0700 Subject: [PATCH 50/58] fix typo in docs/execution --- docs/execution/execute.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/execution/execute.rst b/docs/execution/execute.rst index 0e4de5ac..17bd9071 100644 --- a/docs/execution/execute.rst +++ b/docs/execution/execute.rst @@ -6,11 +6,11 @@ For executing a query a schema, you can directly call the ``execute`` method on .. 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. +``result`` represents the 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 @@ -26,7 +26,7 @@ You can pass context to a query via ``context_value``. def resolve_name(self, args, context, info): return context.get('name') - + schema = graphene.Schema(Query) result = schema.execute('{ name }', context_value={'name': 'Syrus'}) From b5d15b635eabcf6004e222e77c6eb9ad0963cb32 Mon Sep 17 00:00:00 2001 From: Ryan Wilson-Perkin Date: Wed, 3 May 2017 10:58:56 -0400 Subject: [PATCH 51/58] Update UPGRADE-v1.0.md Hoping to help out by fixing a few small grammatical mistakes. Thanks for the upgrade guide! --- UPGRADE-v1.0.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/UPGRADE-v1.0.md b/UPGRADE-v1.0.md index e0f3c4de..a1ab5bd4 100644 --- a/UPGRADE-v1.0.md +++ b/UPGRADE-v1.0.md @@ -97,7 +97,7 @@ schema = graphene.Schema( ## Interfaces -For implementing an Interface in a ObjectType, you have to it onto `Meta.interfaces`. +For implementing an Interface in an ObjectType, you have to add it onto `Meta.interfaces`. Like: @@ -142,7 +142,7 @@ class Query(ObjectType): ## Nodes -Apart of implementing as showed in the previous section, for use the node field you have to +Apart from implementing as shown in the previous section, to use the node field you have to specify the node Type. Example: @@ -155,16 +155,16 @@ class Query(ObjectType): node = relay.Node.Field() # New way ``` -Also, if wanted to create an `ObjectType` that implements `Node`, you have to do it +Also, if you wanted to create an `ObjectType` that implements `Node`, you have to do it explicity. ## Django -The Django integration with Graphene now have an independent package: `graphene-django`. +The Django integration with Graphene now has an independent package: `graphene-django`. For installing, you have to replace the old `graphene[django]` with `graphene-django`. -* As the package is now independent, you have to import now from `graphene_django`. +* As the package is now independent, you now have to import from `graphene_django`. * **DjangoNode no longer exists**, please use `relay.Node` instead: ```python @@ -178,7 +178,7 @@ For installing, you have to replace the old `graphene[django]` with `graphene-dj ## SQLAlchemy -The SQLAlchemy integration with Graphene now have an independent package: `graphene-sqlalchemy`. +The SQLAlchemy integration with Graphene now has an independent package: `graphene-sqlalchemy`. For installing, you have to replace the old `graphene[sqlalchemy]` with `graphene-sqlalchemy`. * As the package is now independent, you have to import now from `graphene_sqlalchemy`. From 59f4ddcd94dbdf1df00e0d23d4569d65e190f4dd Mon Sep 17 00:00:00 2001 From: Daniel Johnston Date: Fri, 5 May 2017 14:23:21 -0700 Subject: [PATCH 52/58] Removed white space. --- graphene/types/tests/test_argument.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/tests/test_argument.py b/graphene/types/tests/test_argument.py index b4cc3d58..5bec9b24 100644 --- a/graphene/types/tests/test_argument.py +++ b/graphene/types/tests/test_argument.py @@ -48,7 +48,7 @@ def test_to_arguments_raises_if_field(): 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).' From 7642644d82cf287d83aaf4779dabfa83ef739184 Mon Sep 17 00:00:00 2001 From: Daniel Johnston Date: Fri, 5 May 2017 14:23:51 -0700 Subject: [PATCH 53/58] Removed white space. --- graphene/types/tests/test_argument.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/types/tests/test_argument.py b/graphene/types/tests/test_argument.py index 5bec9b24..558afd44 100644 --- a/graphene/types/tests/test_argument.py +++ b/graphene/types/tests/test_argument.py @@ -59,5 +59,5 @@ def test_to_arguments_raises_if_inputfield(): 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 388253ede47c2b998c0fb20030f86d908fb383ce Mon Sep 17 00:00:00 2001 From: Daniel Johnston Date: Fri, 5 May 2017 14:27:46 -0700 Subject: [PATCH 54/58] Added type consistency. --- graphene/types/argument.py | 7 ++++++- graphene/types/tests/test_argument.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/graphene/types/argument.py b/graphene/types/argument.py index c884b017..cb28ee5e 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -4,6 +4,7 @@ from itertools import chain from .mountedtype import MountedType from .structures import NonNull from .dynamic import Dynamic +from .utils import get_type class Argument(MountedType): @@ -15,10 +16,14 @@ class Argument(MountedType): type = NonNull(type) self.name = name - self.type = type + self._type = type self.default_value = default_value self.description = description + @property + def type(self): + return get_type(self._type) + def __eq__(self, other): return isinstance(other, Argument) and ( self.name == other.name, diff --git a/graphene/types/tests/test_argument.py b/graphene/types/tests/test_argument.py index 558afd44..256df86b 100644 --- a/graphene/types/tests/test_argument.py +++ b/graphene/types/tests/test_argument.py @@ -1,4 +1,5 @@ import pytest +from functools import partial from ..argument import Argument, to_arguments from ..field import Field @@ -61,3 +62,15 @@ def test_to_arguments_raises_if_inputfield(): to_arguments(args) assert str(exc_info.value) == 'Expected arg_string to be Argument, but received InputField. Try using Argument(String).' + + +def test_argument_with_lazy_type(): + MyType = object() + arg = Field(lambda: MyType) + assert arg.type == MyType + + +def test_argument_with_lazy_partial_type(): + MyType = object() + arg = Field(partial(lambda: MyType)) + assert arg.type == MyType \ No newline at end of file From 83857bfcfe02c9eb7d9baee367d54c0e2d887e9d Mon Sep 17 00:00:00 2001 From: Daniel Johnston Date: Fri, 5 May 2017 14:38:46 -0700 Subject: [PATCH 55/58] Fixed typo. --- graphene/types/tests/test_argument.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene/types/tests/test_argument.py b/graphene/types/tests/test_argument.py index 256df86b..e485c01b 100644 --- a/graphene/types/tests/test_argument.py +++ b/graphene/types/tests/test_argument.py @@ -66,11 +66,11 @@ def test_to_arguments_raises_if_inputfield(): def test_argument_with_lazy_type(): MyType = object() - arg = Field(lambda: MyType) + arg = Argument(lambda: MyType) assert arg.type == MyType def test_argument_with_lazy_partial_type(): MyType = object() - arg = Field(partial(lambda: MyType)) + arg = Argument(partial(lambda: MyType)) assert arg.type == MyType \ No newline at end of file From 0a9dbb608a0ee71dd556c0bd206a0b39bd5a337d Mon Sep 17 00:00:00 2001 From: Ryan Ashcraft Date: Mon, 15 May 2017 19:06:23 -0700 Subject: [PATCH 56/58] Fix typo in dataloader docs --- docs/execution/dataloader.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index 4e6e60dc..2948781d 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -89,7 +89,7 @@ there could be at most 13 database requests! When using DataLoader, we could define the User type using our previous example with -learer code and at most 4 database requests, and possibly fewer if there are cache hits. +leaner code and at most 4 database requests, and possibly fewer if there are cache hits. .. code:: python From 9d30136095551108fad5ee63b3f479b31c818bd6 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 17 May 2017 11:05:53 +0200 Subject: [PATCH 57/58] Fix typo Grapehne -> Graphene --- docs/execution/dataloader.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index 2948781d..3695fcf7 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -59,7 +59,7 @@ maintain minimal outgoing data requests. Using with Graphene ------------------- -DataLoader pairs nicely well with Grapehne/GraphQL. GraphQL fields are designed +DataLoader pairs nicely well with Graphene/GraphQL. GraphQL fields are designed to be stand-alone functions. Without a caching or batching mechanism, it's easy for a naive GraphQL server to issue new database requests each time a field is resolved. From d7dff53f46967601a9b289a075a8bf7190431282 Mon Sep 17 00:00:00 2001 From: "Peter M. Landwehr" Date: Mon, 22 May 2017 16:18:33 -0700 Subject: [PATCH 58/58] Include license in manifest for source bundles --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index a24c614f..9f92821d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ global-exclude tests/* recursive-exclude tests * recursive-exclude tests_py35 * recursive-exclude examples * +include LICENSE