diff --git a/.travis.yml b/.travis.yml index 72e752a6..f80f2790 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,10 +21,7 @@ before_install: source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" fi install: -- pip install pytest-cov pytest-mock coveralls flake8 gevent==1.1b5 six>=1.10.0 promise>=0.4.2 - pytest-benchmark -- pip install pytest==2.9.2 -- pip install -e . +- pip install -e .[test] script: - py.test --cov=graphql graphql tests after_success: @@ -37,6 +34,7 @@ matrix: script: - py.test --cov=graphql graphql tests tests_py35 - python: '2.7' + install: pip install flake8 script: - flake8 deploy: diff --git a/graphql/error/tests/test_base.py b/graphql/error/tests/test_base.py index 0a070e2c..888285c7 100644 --- a/graphql/error/tests/test_base.py +++ b/graphql/error/tests/test_base.py @@ -47,4 +47,14 @@ def resolver(context, *_): ('resolve_or_error', 'return executor.execute(resolve_fn, source, args, context, info)'), ('execute', 'return fn(*args, **kwargs)'), ('resolver', "raise Exception('Failed')") ] + # assert formatted_tb == [ + # ('test_reraise', 'result.errors[0].reraise()'), + # ('reraise', 'six.reraise(type(self), self, self.stack)'), + # ('on_complete_resolver', 'result = __resolver(*args, **kwargs)'), + # # ('reraise', 'raise value.with_traceback(tb)'), + # # ('resolve_or_error', 'return executor.execute(resolve_fn, source, args, context, info)'), + # # ('execute', 'return fn(*args, **kwargs)'), + # ('resolver', "raise Exception('Failed')") + # ] + assert str(exc_info.value) == 'Failed' diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index 7d08db32..1430069e 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -4,7 +4,7 @@ import sys from six import string_types -from promise import Promise, promise_for_dict, promisify, is_thenable +from promise import Promise, promise_for_dict, is_thenable from ..error import GraphQLError, GraphQLLocatedError from ..pyutils.default_ordered_dict import DefaultOrderedDict @@ -16,6 +16,7 @@ collect_fields, default_resolve_fn, get_field_def, get_operation_root_type) from .executors.sync import SyncExecutor +from .experimental.executor import execute as experimental_execute from .middleware import MiddlewareManager logger = logging.getLogger(__name__) @@ -25,9 +26,19 @@ def is_promise(obj): return type(obj) == Promise +use_experimental_executor = False + + def execute(schema, document_ast, root_value=None, context_value=None, variable_values=None, operation_name=None, executor=None, return_promise=False, middleware=None): + if use_experimental_executor: + return experimental_execute( + schema, document_ast, root_value, context_value, + variable_values, operation_name, executor, + return_promise, middleware + ) + assert schema, 'Must provide schema' assert isinstance(schema, GraphQLSchema), ( 'Schema must be an instance of GraphQLSchema. Also ensure that there are ' + @@ -106,7 +117,7 @@ def collect_result(resolved_result): results[response_name] = resolved_result return results - return promisify(result).then(collect_result, None) + return result.then(collect_result, None) results[response_name] = result return results @@ -210,9 +221,9 @@ def complete_value_catching_error(exe_context, return_type, field_asts, info, re if is_thenable(completed): def handle_error(error): exe_context.errors.append(error) - return Promise.fulfilled(None) + return None - return promisify(completed).then(None, handle_error) + return completed.catch(handle_error) return completed except Exception as e: @@ -242,7 +253,7 @@ def complete_value(exe_context, return_type, field_asts, info, result): # If field type is NonNull, complete for inner type, and throw field error if result is null. if is_thenable(result): - return promisify(result).then( + return Promise.resolve(result).then( lambda resolved: complete_value( exe_context, return_type, diff --git a/graphql/execution/executors/asyncio.py b/graphql/execution/executors/asyncio.py index 552c475f..0aec27c2 100644 --- a/graphql/execution/executors/asyncio.py +++ b/graphql/execution/executors/asyncio.py @@ -2,7 +2,7 @@ from asyncio import Future, get_event_loop, iscoroutine, wait -from promise import promisify +from promise import Promise try: from asyncio import ensure_future @@ -49,5 +49,5 @@ def execute(self, fn, *args, **kwargs): if isinstance(result, Future) or iscoroutine(result): future = ensure_future(result, loop=self.loop) self.futures.append(future) - return promisify(future) + return Promise.resolve(future) return result diff --git a/graphql/execution/executors/utils.py b/graphql/execution/executors/utils.py index 79b67cbe..4fc44875 100644 --- a/graphql/execution/executors/utils.py +++ b/graphql/execution/executors/utils.py @@ -1,6 +1,6 @@ def process(p, f, args, kwargs): try: val = f(*args, **kwargs) - p.fulfill(val) + p.do_resolve(val) except Exception as e: - p.reject(e) + p.do_reject(e) diff --git a/graphql/execution/experimental/__init__.py b/graphql/execution/experimental/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphql/execution/experimental/executor.py b/graphql/execution/experimental/executor.py new file mode 100644 index 00000000..401fbfa8 --- /dev/null +++ b/graphql/execution/experimental/executor.py @@ -0,0 +1,64 @@ +from promise import Promise + +from ...type import GraphQLSchema +from ..base import ExecutionContext, ExecutionResult, get_operation_root_type +from ..executors.sync import SyncExecutor +from ..middleware import MiddlewareManager +from .fragment import Fragment + + +def execute(schema, document_ast, root_value=None, context_value=None, + variable_values=None, operation_name=None, executor=None, + return_promise=False, middleware=None): + assert schema, 'Must provide schema' + assert isinstance(schema, GraphQLSchema), ( + 'Schema must be an instance of GraphQLSchema. Also ensure that there are ' + + 'not multiple versions of GraphQL installed in your node_modules directory.' + ) + if middleware: + if not isinstance(middleware, MiddlewareManager): + middleware = MiddlewareManager(*middleware) + assert isinstance(middleware, MiddlewareManager), ( + 'middlewares have to be an instance' + ' of MiddlewareManager. Received "{}".'.format(middleware) + ) + + if executor is None: + executor = SyncExecutor() + + context = ExecutionContext( + schema, + document_ast, + root_value, + context_value, + variable_values, + operation_name, + executor, + middleware + ) + + def executor(resolve, reject): + return resolve(execute_operation(context, context.operation, root_value)) + + def on_rejected(error): + context.errors.append(error) + return None + + def on_resolve(data): + return ExecutionResult(data=data, errors=context.errors) + + promise = Promise(executor).catch(on_rejected).then(on_resolve) + if return_promise: + return promise + context.executor.wait_until_finished() + return promise.get() + + +def execute_operation(exe_context, operation, root_value): + type = get_operation_root_type(exe_context.schema, operation) + execute_serially = operation.operation == 'mutation' + + fragment = Fragment(type=type, field_asts=[operation], context=exe_context) + if execute_serially: + return fragment.resolve_serially(root_value) + return fragment.resolve(root_value) diff --git a/graphql/execution/experimental/fragment.py b/graphql/execution/experimental/fragment.py new file mode 100644 index 00000000..427acbaf --- /dev/null +++ b/graphql/execution/experimental/fragment.py @@ -0,0 +1,252 @@ +import functools + +from promise import Promise, is_thenable, promise_for_dict + +from ...pyutils.cached_property import cached_property +from ...pyutils.default_ordered_dict import DefaultOrderedDict +from ...type import (GraphQLInterfaceType, GraphQLList, GraphQLNonNull, + GraphQLObjectType, GraphQLUnionType) +from ..base import ResolveInfo, Undefined, collect_fields, get_field_def +from ..values import get_argument_values +from ...error import GraphQLError +try: + from itertools import izip as zip +except: + pass + + +def get_base_type(type): + if isinstance(type, (GraphQLList, GraphQLNonNull)): + return get_base_type(type.of_type) + return type + + +def get_subfield_asts(context, return_type, field_asts): + subfield_asts = DefaultOrderedDict(list) + visited_fragment_names = set() + for field_ast in field_asts: + selection_set = field_ast.selection_set + if selection_set: + subfield_asts = collect_fields( + context, return_type, selection_set, + subfield_asts, visited_fragment_names + ) + return subfield_asts + + +def get_resolvers(context, type, field_asts): + from .resolver import field_resolver + subfield_asts = get_subfield_asts(context, type, field_asts) + + for response_name, field_asts in subfield_asts.items(): + field_ast = field_asts[0] + field_name = field_ast.name.value + field_def = get_field_def(context and context.schema, type, field_name) + if not field_def: + continue + field_base_type = get_base_type(field_def.type) + field_fragment = None + info = ResolveInfo( + field_name, + field_asts, + field_base_type, + parent_type=type, + schema=context and context.schema, + fragments=context and context.fragments, + root_value=context and context.root_value, + operation=context and context.operation, + variable_values=context and context.variable_values, + ) + if isinstance(field_base_type, GraphQLObjectType): + field_fragment = Fragment( + type=field_base_type, + field_asts=field_asts, + info=info, + context=context + ) + elif isinstance(field_base_type, (GraphQLInterfaceType, GraphQLUnionType)): + field_fragment = AbstractFragment( + abstract_type=field_base_type, + field_asts=field_asts, + info=info, + context=context + ) + resolver = field_resolver(field_def, exe_context=context, info=info, fragment=field_fragment) + args = get_argument_values( + field_def.args, + field_ast.arguments, + context and context.variable_values + ) + yield (response_name, Field(resolver, args, context and context.context_value, info)) + + +class Field(object): + __slots__ = ('fn', 'args', 'context', 'info') + + def __init__(self, fn, args, context, info): + self.fn = fn + self.args = args + self.context = context + self.info = info + + def execute(self, root): + return self.fn(root, self.args, self.context, self.info) + + +class Fragment(object): + + def __init__(self, type, field_asts, context=None, info=None): + self.type = type + self.field_asts = field_asts + self.context = context + self.info = info + + @cached_property + def partial_resolvers(self): + return list(get_resolvers( + self.context, + self.type, + self.field_asts + )) + + @cached_property + def fragment_container(self): + try: + fields = next(zip(*self.partial_resolvers)) + except StopIteration: + fields = tuple() + + class FragmentInstance(dict): + # def __init__(self): + # self.fields = fields + # _fields = ('c','b','a') + set = dict.__setitem__ + # def set(self, name, value): + # self[name] = value + + def __iter__(self): + return iter(fields) + + return FragmentInstance + + def have_type(self, root): + return not self.type.is_type_of or self.type.is_type_of(root, self.context.context_value, self.info) + + def resolve(self, root): + if root and not self.have_type(root): + raise GraphQLError( + u'Expected value of type "{}" but got: {}.'.format(self.type, type(root).__name__), + self.info.field_asts + ) + + contains_promise = False + + final_results = self.fragment_container() + # return OrderedDict( + # ((field_name, field_resolver(root, field_args, context, info)) + # for field_name, field_resolver, field_args, context, info in self.partial_resolvers) + # ) + for response_name, field_resolver in self.partial_resolvers: + + result = field_resolver.execute(root) + if result is Undefined: + continue + + if not contains_promise and is_thenable(result): + contains_promise = True + + final_results[response_name] = result + + if not contains_promise: + return final_results + + return promise_for_dict(final_results) + # return { + # field_name: field_resolver(root, field_args, context, info) + # for field_name, field_resolver, field_args, context, info in self.partial_resolvers + # } + + def resolve_serially(self, root): + def execute_field_callback(results, resolver): + response_name, field_resolver = resolver + + result = field_resolver.execute(root) + + if result is Undefined: + return results + + if is_thenable(result): + def collect_result(resolved_result): + results[response_name] = resolved_result + return results + + return result.then(collect_result) + + results[response_name] = result + return results + + def execute_field(prev_promise, resolver): + return prev_promise.then(lambda results: execute_field_callback(results, resolver)) + + return functools.reduce(execute_field, self.partial_resolvers, Promise.resolve(self.fragment_container())) + + def __eq__(self, other): + return isinstance(other, Fragment) and ( + other.type == self.type and + other.field_asts == self.field_asts and + other.context == self.context and + other.info == self.info + ) + + +class AbstractFragment(object): + + def __init__(self, abstract_type, field_asts, context=None, info=None): + self.abstract_type = abstract_type + self.field_asts = field_asts + self.context = context + self.info = info + self._fragments = {} + + @cached_property + def possible_types(self): + return self.context.schema.get_possible_types(self.abstract_type) + + @cached_property + def possible_types_with_is_type_of(self): + return [ + (type, type.is_type_of) for type in self.possible_types if callable(type.is_type_of) + ] + + def get_fragment(self, type): + if isinstance(type, str): + type = self.context.schema.get_type(type) + + if type not in self._fragments: + assert type in self.possible_types, ( + 'Runtime Object type "{}" is not a possible type for "{}".' + ).format(type, self.abstract_type) + self._fragments[type] = Fragment( + type, + self.field_asts, + self.context, + self.info + ) + + return self._fragments[type] + + def resolve_type(self, result): + return_type = self.abstract_type + context = self.context.context_value + + if return_type.resolve_type: + return return_type.resolve_type(result, context, self.info) + + for type, is_type_of in self.possible_types_with_is_type_of: + if is_type_of(result, context, self.info): + return type + + def resolve(self, root): + _type = self.resolve_type(root) + fragment = self.get_fragment(_type) + return fragment.resolve(root) diff --git a/graphql/execution/experimental/resolver.py b/graphql/execution/experimental/resolver.py new file mode 100644 index 00000000..75f5b7d3 --- /dev/null +++ b/graphql/execution/experimental/resolver.py @@ -0,0 +1,151 @@ +import sys +import collections +from functools import partial + +from promise import Promise, is_thenable + +from ...error import GraphQLError, GraphQLLocatedError +from ...type import (GraphQLEnumType, GraphQLInterfaceType, GraphQLList, + GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, + GraphQLUnionType) +from ..base import default_resolve_fn +from ...execution import executor +from .utils import imap, normal_map + + +def on_complete_resolver(on_error, __func, exe_context, info, __resolver, *args, **kwargs): + try: + result = __resolver(*args, **kwargs) + if isinstance(result, Exception): + return on_error(result) + # return Promise.resolve(result).then(__func).catch(on_error) + if is_thenable(result): + # TODO: Remove this, if a promise is resolved with an Exception, + # it should raise by default. This is fixing an old behavior + # in the Promise package + def on_resolve(value): + if isinstance(value, Exception): + return on_error(value) + return value + return result.then(on_resolve).then(__func).catch(on_error) + return __func(result) + except Exception as e: + return on_error(e) + + +def complete_list_value(inner_resolver, exe_context, info, on_error, result): + if result is None: + return None + + assert isinstance(result, collections.Iterable), \ + ('User Error: expected iterable, but did not find one ' + + 'for field {}.{}.').format(info.parent_type, info.field_name) + + completed_results = normal_map(inner_resolver, result) + + if not any(imap(is_thenable, completed_results)): + return completed_results + + return Promise.all(completed_results).catch(on_error) + + +def complete_nonnull_value(exe_context, info, result): + if result is None: + raise GraphQLError( + 'Cannot return null for non-nullable field {}.{}.'.format(info.parent_type, info.field_name), + info.field_asts + ) + return result + + +def complete_leaf_value(serialize, result): + if result is None: + return None + return serialize(result) + + +def complete_object_value(fragment_resolve, exe_context, on_error, result): + if result is None: + return None + + result = fragment_resolve(result) + if is_thenable(result): + return result.catch(on_error) + return result + + +def field_resolver(field, fragment=None, exe_context=None, info=None): + # resolver = exe_context.get_field_resolver(field.resolver or default_resolve_fn) + resolver = field.resolver or default_resolve_fn + if exe_context: + # We decorate the resolver with the middleware + resolver = exe_context.get_field_resolver(resolver) + return type_resolver(field.type, resolver, + fragment, exe_context, info, catch_error=True) + + +def type_resolver(return_type, resolver, fragment=None, exe_context=None, info=None, catch_error=False): + if isinstance(return_type, GraphQLNonNull): + return type_resolver_non_null(return_type, resolver, fragment, exe_context, info) + + if isinstance(return_type, (GraphQLScalarType, GraphQLEnumType)): + return type_resolver_leaf(return_type, resolver, exe_context, info, catch_error) + + if isinstance(return_type, (GraphQLList)): + return type_resolver_list(return_type, resolver, fragment, exe_context, info, catch_error) + + if isinstance(return_type, (GraphQLObjectType)): + assert fragment and fragment.type == return_type, 'Fragment and return_type dont match' + return type_resolver_fragment(return_type, resolver, fragment, exe_context, info, catch_error) + + if isinstance(return_type, (GraphQLInterfaceType, GraphQLUnionType)): + assert fragment, 'You need to pass a fragment to resolve a Interface or Union' + return type_resolver_fragment(return_type, resolver, fragment, exe_context, info, catch_error) + + raise Exception("The resolver have to be created for a fragment") + + +def on_error(exe_context, info, catch_error, e): + error = e + if not isinstance(e, (GraphQLLocatedError, GraphQLError)): + error = GraphQLLocatedError(info.field_asts, original_error=e) + if catch_error: + exe_context.errors.append(error) + executor.logger.exception("An error occurred while resolving field {}.{}".format( + info.parent_type.name, info.field_name + )) + error.stack = sys.exc_info()[2] + return None + raise error + + +def type_resolver_fragment(return_type, resolver, fragment, exe_context, info, catch_error): + on_complete_type_error = partial(on_error, exe_context, info, catch_error) + complete_object_value_resolve = partial( + complete_object_value, + fragment.resolve, + exe_context, + on_complete_type_error) + on_resolve_error = partial(on_error, exe_context, info, catch_error) + return partial(on_complete_resolver, on_resolve_error, complete_object_value_resolve, exe_context, info, resolver) + + +def type_resolver_non_null(return_type, resolver, fragment, exe_context, info): # no catch_error + resolver = type_resolver(return_type.of_type, resolver, fragment, exe_context, info) + nonnull_complete = partial(complete_nonnull_value, exe_context, info) + on_resolve_error = partial(on_error, exe_context, info, False) + return partial(on_complete_resolver, on_resolve_error, nonnull_complete, exe_context, info, resolver) + + +def type_resolver_leaf(return_type, resolver, exe_context, info, catch_error): + leaf_complete = partial(complete_leaf_value, return_type.serialize) + on_resolve_error = partial(on_error, exe_context, info, catch_error) + return partial(on_complete_resolver, on_resolve_error, leaf_complete, exe_context, info, resolver) + + +def type_resolver_list(return_type, resolver, fragment, exe_context, info, catch_error): + item_type = return_type.of_type + inner_resolver = type_resolver(item_type, lambda item: item, fragment, exe_context, info, catch_error=True) + on_resolve_error = partial(on_error, exe_context, info, catch_error) + list_complete = partial(complete_list_value, inner_resolver, exe_context, info, on_resolve_error) + return partial(on_complete_resolver, on_resolve_error, list_complete, exe_context, info, resolver) diff --git a/graphql/execution/experimental/tests/__init__.py b/graphql/execution/experimental/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphql/execution/experimental/tests/skip_test_benchmark.py b/graphql/execution/experimental/tests/skip_test_benchmark.py new file mode 100644 index 00000000..407a7dc1 --- /dev/null +++ b/graphql/execution/experimental/tests/skip_test_benchmark.py @@ -0,0 +1,118 @@ +# import pytest + +# from promise import Promise + +# from ....language import ast +# from ....type import (GraphQLEnumType, GraphQLField, GraphQLInt, +# GraphQLInterfaceType, GraphQLList, GraphQLNonNull, +# GraphQLObjectType, GraphQLScalarType, GraphQLSchema, +# GraphQLString, GraphQLUnionType) +# from ..fragment import Fragment +# from ..resolver import type_resolver + +# SIZE = 10000 + + +# def test_experimental_big_list_of_ints(benchmark): +# big_int_list = [x for x in range(SIZE)] + +# resolver = type_resolver(GraphQLList(GraphQLInt), lambda: big_int_list) +# result = benchmark(resolver) + +# assert result == big_int_list + + +# def test_experimental_big_list_of_nested_ints(benchmark): +# big_int_list = [x for x in range(SIZE)] + +# Node = GraphQLObjectType( +# 'Node', +# fields={ +# 'id': GraphQLField( +# GraphQLInt, +# resolver=lambda obj, +# args, +# context, +# info: obj)}) +# selection_set = ast.SelectionSet(selections=[ +# ast.Field( +# alias=None, +# name=ast.Name(value='id'), +# arguments=[], +# directives=[], +# selection_set=None +# ) +# ]) +# fragment = Fragment(type=Node, selection_set=selection_set) +# type = GraphQLList(Node) +# resolver = type_resolver(type, lambda: big_int_list, fragment=fragment) +# resolved = benchmark(resolver) + +# assert resolved == [{ +# 'id': n +# } for n in big_int_list] + + +# def test_experimental_big_list_of_objecttypes_with_two_int_fields(benchmark): +# big_int_list = [x for x in range(SIZE)] + +# Node = GraphQLObjectType('Node', fields={ +# 'id': GraphQLField(GraphQLInt, resolver=lambda obj, args, context, info: obj), +# 'ida': GraphQLField(GraphQLInt, resolver=lambda obj, args, context, info: obj * 2) +# }) +# selection_set = ast.SelectionSet(selections=[ +# ast.Field( +# alias=None, +# name=ast.Name(value='id'), +# arguments=[], +# directives=[], +# selection_set=None +# ), +# ast.Field( +# alias=None, +# name=ast.Name(value='ida'), +# arguments=[], +# directives=[], +# selection_set=None +# ) +# ]) +# fragment = Fragment(type=Node, selection_set=selection_set) +# type = GraphQLList(Node) +# resolver = type_resolver(type, lambda: big_int_list, fragment=fragment) +# resolved = benchmark(resolver) + +# assert resolved == [{ +# 'id': n, +# 'ida': n * 2 +# } for n in big_int_list] + + +# def test_experimental_big_list_of_objecttypes_with_one_int_field(benchmark): +# big_int_list = [x for x in range(SIZE)] +# Node = GraphQLObjectType('Node', fields={'id': GraphQLField(GraphQLInt, resolver=lambda obj, *_, **__: obj)}) +# Query = GraphQLObjectType( +# 'Query', +# fields={ +# 'nodes': GraphQLField( +# GraphQLList(Node), +# resolver=lambda *_, +# **__: big_int_list)}) +# node_selection_set = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='id'), +# ) +# ]) +# selection_set = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='nodes'), +# selection_set=node_selection_set +# ) +# ]) +# query_fragment = Fragment(type=Query, selection_set=selection_set) +# resolver = type_resolver(Query, lambda: object(), fragment=query_fragment) +# resolved = benchmark(resolver) +# assert resolved == { +# 'nodes': [{ +# 'id': n +# } for n in big_int_list] +# } diff --git a/graphql/execution/experimental/tests/skip_test_fragment.py b/graphql/execution/experimental/tests/skip_test_fragment.py new file mode 100644 index 00000000..b891f675 --- /dev/null +++ b/graphql/execution/experimental/tests/skip_test_fragment.py @@ -0,0 +1,197 @@ +# import pytest + +# from promise import Promise + +# from ....language import ast +# from ....language.parser import parse +# from ....type import (GraphQLEnumType, GraphQLField, GraphQLInt, +# GraphQLInterfaceType, GraphQLList, GraphQLNonNull, +# GraphQLObjectType, GraphQLScalarType, GraphQLSchema, +# GraphQLString, GraphQLUnionType) +# from ...base import ExecutionContext +# from ..fragment import Fragment +# from ..resolver import type_resolver + + +# def test_fragment_equal(): +# selection1 = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='id'), +# ) +# ]) +# selection2 = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='id'), +# ) +# ]) +# assert selection1 == selection2 +# Node = GraphQLObjectType('Node', fields={'id': GraphQLField(GraphQLInt)}) +# Node2 = GraphQLObjectType('Node2', fields={'id': GraphQLField(GraphQLInt)}) +# fragment1 = Fragment(type=Node, selection_set=selection1) +# fragment2 = Fragment(type=Node, selection_set=selection2) +# fragment3 = Fragment(type=Node2, selection_set=selection2) +# assert fragment1 == fragment2 +# assert fragment1 != fragment3 +# assert fragment1 != object() + + +# def test_fragment_resolver(): +# Node = GraphQLObjectType('Node', fields={'id': GraphQLField(GraphQLInt, resolver=lambda obj, *_, **__: obj * 2)}) +# selection_set = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='id'), +# ) +# ]) +# fragment = Fragment(type=Node, selection_set=selection_set) +# assert fragment.resolve(1) == {'id': 2} +# assert fragment.resolve(2) == {'id': 4} + + +# def test_fragment_resolver_list(): +# Node = GraphQLObjectType('Node', fields={'id': GraphQLField(GraphQLInt, resolver=lambda obj, *_, **__: obj)}) +# selection_set = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='id'), +# ) +# ]) +# fragment = Fragment(type=Node, selection_set=selection_set) +# type = GraphQLList(Node) + +# resolver = type_resolver(type, lambda: range(3), fragment=fragment) +# resolved = resolver() +# assert resolved == [{ +# 'id': n +# } for n in range(3)] + + +# def test_fragment_resolver_nested(): +# Node = GraphQLObjectType('Node', fields={'id': GraphQLField(GraphQLInt, resolver=lambda obj, *_, **__: obj)}) +# Query = GraphQLObjectType('Query', fields={'node': GraphQLField(Node, resolver=lambda *_, **__: 1)}) +# node_selection_set = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='id'), +# ) +# ]) +# selection_set = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='node'), +# selection_set=node_selection_set +# ) +# ]) +# # node_fragment = Fragment(type=Node, field_asts=node_field_asts) +# query_fragment = Fragment(type=Query, selection_set=selection_set) +# resolver = type_resolver(Query, lambda: object(), fragment=query_fragment) +# resolved = resolver() +# assert resolved == { +# 'node': { +# 'id': 1 +# } +# } + + +# def test_fragment_resolver_abstract(): +# Node = GraphQLInterfaceType('Node', fields={'id': GraphQLField(GraphQLInt)}) +# Person = GraphQLObjectType( +# 'Person', +# interfaces=( +# Node, +# ), +# is_type_of=lambda *_: True, +# fields={ +# 'id': GraphQLField( +# GraphQLInt, +# resolver=lambda obj, +# *_, +# **__: obj)}) +# Query = GraphQLObjectType('Query', fields={'node': GraphQLField(Node, resolver=lambda *_, **__: 1)}) +# node_selection_set = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='id'), +# ) +# ]) +# selection_set = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='node'), +# selection_set=node_selection_set +# ) +# ]) +# # node_fragment = Fragment(type=Node, field_asts=node_field_asts) +# schema = GraphQLSchema(query=Query, types=[Person]) +# document_ast = parse('''{ +# node { +# id +# } +# }''') + +# root_value = None +# context_value = None +# operation_name = None +# variable_values = {} +# executor = None +# middlewares = None +# context = ExecutionContext( +# schema, +# document_ast, +# root_value, +# context_value, +# variable_values, +# operation_name, +# executor, +# middlewares +# ) + +# query_fragment = Fragment(type=Query, selection_set=selection_set, context=context) +# resolver = type_resolver(Query, lambda: object(), fragment=query_fragment) +# resolved = resolver() +# assert resolved == { +# 'node': { +# 'id': 1 +# } +# } + + +# def test_fragment_resolver_nested_list(): +# Node = GraphQLObjectType('Node', fields={'id': GraphQLField(GraphQLInt, resolver=lambda obj, *_, **__: obj)}) +# Query = GraphQLObjectType( +# 'Query', +# fields={ +# 'nodes': GraphQLField( +# GraphQLList(Node), +# resolver=lambda *_, +# **__: range(3))}) +# node_selection_set = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='id'), +# ) +# ]) +# selection_set = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='nodes'), +# selection_set=node_selection_set +# ) +# ]) +# # node_fragment = Fragment(type=Node, field_asts=node_field_asts) +# query_fragment = Fragment(type=Query, selection_set=selection_set) +# resolver = type_resolver(Query, lambda: object(), fragment=query_fragment) +# resolved = resolver() +# assert resolved == { +# 'nodes': [{ +# 'id': n +# } for n in range(3)] +# } + +# # ''' +# # { +# # books { +# # title +# # author { +# # name +# # } +# # } +# # }''' +# # BooksFragment( +# # ('title', str(resolve_title())), +# # ('author', AuthorFragment( +# # ('name', str(resolve_author())) +# # )) +# # ) diff --git a/graphql/execution/experimental/tests/skip_test_resolver.py b/graphql/execution/experimental/tests/skip_test_resolver.py new file mode 100644 index 00000000..a8ed4715 --- /dev/null +++ b/graphql/execution/experimental/tests/skip_test_resolver.py @@ -0,0 +1,186 @@ +import mock +import pytest + +from promise import Promise + +from ....error import GraphQLError, GraphQLLocatedError +from ....language import ast +from ....type import (GraphQLEnumType, GraphQLField, GraphQLInt, + GraphQLInterfaceType, GraphQLList, GraphQLNonNull, + GraphQLObjectType, GraphQLScalarType, GraphQLSchema, + GraphQLString, GraphQLUnionType) +from ..fragment import Fragment +from ..resolver import field_resolver, type_resolver + + +@pytest.mark.parametrize("type,value,expected", [ + (GraphQLString, 1, "1"), + (GraphQLInt, "1", 1), + (GraphQLNonNull(GraphQLString), 0, "0"), + (GraphQLNonNull(GraphQLInt), 0, 0), + (GraphQLList(GraphQLString), [1, 2], ['1', '2']), + (GraphQLList(GraphQLInt), ['1', '2'], [1, 2]), + (GraphQLList(GraphQLNonNull(GraphQLInt)), [0], [0]), + (GraphQLNonNull(GraphQLList(GraphQLInt)), [], []), +]) +def test_type_resolver(type, value, expected): + resolver = type_resolver(type, lambda: value) + resolved = resolver() + assert resolved == expected + + +@pytest.mark.parametrize("type,value,expected", [ + (GraphQLString, 1, "1"), + (GraphQLInt, "1", 1), + (GraphQLNonNull(GraphQLString), 0, "0"), + (GraphQLNonNull(GraphQLInt), 0, 0), + (GraphQLList(GraphQLString), [1, 2], ['1', '2']), + (GraphQLList(GraphQLInt), ['1', '2'], [1, 2]), + (GraphQLList(GraphQLNonNull(GraphQLInt)), [0], [0]), + (GraphQLNonNull(GraphQLList(GraphQLInt)), [], []), +]) +def test_type_resolver_promise(type, value, expected): + promise_value = Promise() + resolver = type_resolver(type, lambda: promise_value) + resolved_promise = resolver() + assert not resolved_promise.is_fulfilled + promise_value.fulfill(value) + assert resolved_promise.is_fulfilled + resolved = resolved_promise.get() + assert resolved == expected + + +def raises(): + raise Exception("raises") + + +def test_resolver_exception(): + info = mock.MagicMock() + with pytest.raises(GraphQLLocatedError): + resolver = type_resolver(GraphQLString, raises, info=info) + resolver() + + +def test_field_resolver_mask_exception(): + info = mock.MagicMock() + exe_context = mock.MagicMock() + exe_context.errors = [] + field = GraphQLField(GraphQLString, resolver=raises) + resolver = field_resolver(field, info=info, exe_context=exe_context) + resolved = resolver() + assert resolved is None + assert len(exe_context.errors) == 1 + assert str(exe_context.errors[0]) == 'raises' + + +def test_nonnull_field_resolver_mask_exception(): + info = mock.MagicMock() + info.parent_type = 'parent_type' + info.field_name = 'field_name' + exe_context = mock.MagicMock() + exe_context.errors = [] + field = GraphQLField(GraphQLNonNull(GraphQLString), resolver=raises) + resolver = field_resolver(field, info=info, exe_context=exe_context) + with pytest.raises(GraphQLLocatedError) as exc_info: + resolver() + assert str(exc_info.value) == 'raises' + + +def test_nonnull_field_resolver_fails_on_null_value(): + info = mock.MagicMock() + info.parent_type = 'parent_type' + info.field_name = 'field_name' + exe_context = mock.MagicMock() + exe_context.errors = [] + field = GraphQLField(GraphQLNonNull(GraphQLString), resolver=lambda *_: None) + resolver = field_resolver(field, info=info, exe_context=exe_context) + with pytest.raises(GraphQLError) as exc_info: + resolver() + + assert str(exc_info.value) == 'Cannot return null for non-nullable field parent_type.field_name.' + + +def test_nonnull_list_field_resolver_fails_silently_on_null_value(): + info = mock.MagicMock() + info.parent_type = 'parent_type' + info.field_name = 'field_name' + exe_context = mock.MagicMock() + exe_context.errors = [] + field = GraphQLField(GraphQLList(GraphQLNonNull(GraphQLString)), resolver=lambda *_: ['1', None]) + resolver = field_resolver(field, info=info, exe_context=exe_context) + assert resolver() is None + + assert len(exe_context.errors) == 1 + assert str(exe_context.errors[0]) == 'Cannot return null for non-nullable field parent_type.field_name.' + + +def test_nonnull_list_field_resolver_fails_on_null_value_top(): + from ....pyutils.default_ordered_dict import DefaultOrderedDict + from ...base import collect_fields + + + DataType = GraphQLObjectType('DataType', { + 'nonNullString': GraphQLField(GraphQLNonNull(GraphQLString), resolver=lambda *_: None), + }) + info = mock.MagicMock() + info.parent_type = 'parent_type' + info.field_name = 'field_name' + exe_context = mock.MagicMock() + exe_context.errors = [] + field = GraphQLField(GraphQLNonNull(DataType), resolver=lambda *_: 1) + selection_set = ast.SelectionSet(selections=[ + ast.Field( + name=ast.Name(value='nonNullString'), + ) + ]) + field_asts = collect_fields( + exe_context, + DataType, + selection_set, + DefaultOrderedDict(list), + set() + ) + + # node_fragment = Fragment(type=Node, field_asts=node_field_asts) + datetype_fragment = Fragment(type=DataType, field_asts=field_asts, context=exe_context) + resolver = field_resolver(field, info=info, exe_context=exe_context, fragment=datetype_fragment) + with pytest.raises(GraphQLError) as exc_info: + resolver() + + assert not exe_context.errors + assert str(exc_info.value) == 'Cannot return null for non-nullable field parent_type.field_name.' + + +def test_nonnull_list_field_resolver_fails_on_null_value_top(): + from ....pyutils.default_ordered_dict import DefaultOrderedDict + from ...base import collect_fields + + + DataType = GraphQLObjectType('DataType', { + 'nonNullString': GraphQLField(GraphQLString, resolver=lambda *_: None), + }) + info = mock.MagicMock() + info.parent_type = 'parent_type' + info.field_name = 'field_name' + exe_context = mock.MagicMock() + exe_context.errors = [] + field = GraphQLField(GraphQLNonNull(DataType), resolver=lambda *_: 1) + selection_set = ast.SelectionSet(selections=[ + ast.Field( + name=ast.Name(value='nonNullString'), + ) + ]) + field_asts = collect_fields( + exe_context, + DataType, + selection_set, + DefaultOrderedDict(list), + set() + ) + # node_fragment = Fragment(type=Node, field_asts=node_field_asts) + datetype_fragment = Fragment(type=DataType, field_asts=field_asts, context=exe_context) + resolver = field_resolver(field, info=info, exe_context=exe_context, fragment=datetype_fragment) + data = resolver() + assert data == { + 'nonNullString': None + } diff --git a/graphql/execution/experimental/tests/test_abstract.py b/graphql/execution/experimental/tests/test_abstract.py new file mode 100644 index 00000000..d7ca41f4 --- /dev/null +++ b/graphql/execution/experimental/tests/test_abstract.py @@ -0,0 +1,297 @@ +from graphql import graphql +from graphql.type import GraphQLBoolean, GraphQLSchema, GraphQLString +from graphql.type.definition import (GraphQLField, GraphQLInterfaceType, + GraphQLList, GraphQLObjectType, + GraphQLUnionType) + + +class Dog(object): + + def __init__(self, name, woofs): + self.name = name + self.woofs = woofs + + +class Cat(object): + + def __init__(self, name, meows): + self.name = name + self.meows = meows + + +class Human(object): + + def __init__(self, name): + self.name = name + + +is_type_of = lambda type: lambda obj, context, info: isinstance(obj, type) + + +def make_type_resolver(types): + def resolve_type(obj, context, info): + if callable(types): + t = types() + else: + t = types + + for klass, type in t: + if isinstance(obj, klass): + return type + + return None + + return resolve_type + + +def test_is_type_of_used_to_resolve_runtime_type_for_interface(): + PetType = GraphQLInterfaceType( + name='Pet', + fields={ + 'name': GraphQLField(GraphQLString) + } + ) + + DogType = GraphQLObjectType( + name='Dog', + interfaces=[PetType], + is_type_of=is_type_of(Dog), + fields={ + 'name': GraphQLField(GraphQLString), + 'woofs': GraphQLField(GraphQLBoolean) + } + ) + + CatType = GraphQLObjectType( + name='Cat', + interfaces=[PetType], + is_type_of=is_type_of(Cat), + fields={ + 'name': GraphQLField(GraphQLString), + 'meows': GraphQLField(GraphQLBoolean) + } + ) + + schema = GraphQLSchema( + query=GraphQLObjectType( + name='Query', + fields={ + 'pets': GraphQLField( + GraphQLList(PetType), + resolver=lambda *_: [Dog('Odie', True), Cat('Garfield', False)] + ) + } + ), + types=[CatType, DogType] + ) + + query = ''' + { + pets { + name + ... on Dog { + woofs + } + ... on Cat { + meows + } + } + } + ''' + + result = graphql(schema, query) + assert not result.errors + assert result.data == {'pets': [{'woofs': True, 'name': 'Odie'}, {'name': 'Garfield', 'meows': False}]} + + +def test_is_type_of_used_to_resolve_runtime_type_for_union(): + DogType = GraphQLObjectType( + name='Dog', + is_type_of=is_type_of(Dog), + fields={ + 'name': GraphQLField(GraphQLString), + 'woofs': GraphQLField(GraphQLBoolean) + } + ) + + CatType = GraphQLObjectType( + name='Cat', + is_type_of=is_type_of(Cat), + fields={ + 'name': GraphQLField(GraphQLString), + 'meows': GraphQLField(GraphQLBoolean) + } + ) + + PetType = GraphQLUnionType( + name='Pet', + types=[CatType, DogType] + ) + + schema = GraphQLSchema( + query=GraphQLObjectType( + name='Query', + fields={ + 'pets': GraphQLField( + GraphQLList(PetType), + resolver=lambda *_: [Dog('Odie', True), Cat('Garfield', False)] + ) + } + ), + types=[CatType, DogType] + ) + + query = ''' + { + pets { + ... on Dog { + name + woofs + } + ... on Cat { + name + meows + } + } + } + ''' + + result = graphql(schema, query) + assert not result.errors + assert result.data == {'pets': [{'woofs': True, 'name': 'Odie'}, {'name': 'Garfield', 'meows': False}]} + + +def test_resolve_type_on_interface_yields_useful_error(): + PetType = GraphQLInterfaceType( + name='Pet', + fields={ + 'name': GraphQLField(GraphQLString) + }, + resolve_type=make_type_resolver(lambda: [ + (Dog, DogType), + (Cat, CatType), + (Human, HumanType) + ]) + ) + + DogType = GraphQLObjectType( + name='Dog', + interfaces=[PetType], + fields={ + 'name': GraphQLField(GraphQLString), + 'woofs': GraphQLField(GraphQLBoolean) + } + ) + + HumanType = GraphQLObjectType( + name='Human', + fields={ + 'name': GraphQLField(GraphQLString), + } + ) + + CatType = GraphQLObjectType( + name='Cat', + interfaces=[PetType], + fields={ + 'name': GraphQLField(GraphQLString), + 'meows': GraphQLField(GraphQLBoolean) + } + ) + + schema = GraphQLSchema( + query=GraphQLObjectType( + name='Query', + fields={ + 'pets': GraphQLField( + GraphQLList(PetType), + resolver=lambda *_: [Dog('Odie', True), Cat('Garfield', False), Human('Jon')] + ) + } + ), + types=[DogType, CatType] + ) + + query = ''' + { + pets { + name + ... on Dog { + woofs + } + ... on Cat { + meows + } + } + } + ''' + + result = graphql(schema, query) + assert result.errors[0].message == 'Runtime Object type "Human" is not a possible type for "Pet".' + assert result.data == {'pets': [{'woofs': True, 'name': 'Odie'}, {'name': 'Garfield', 'meows': False}, None]} + + +def test_resolve_type_on_union_yields_useful_error(): + DogType = GraphQLObjectType( + name='Dog', + fields={ + 'name': GraphQLField(GraphQLString), + 'woofs': GraphQLField(GraphQLBoolean) + } + ) + + HumanType = GraphQLObjectType( + name='Human', + fields={ + 'name': GraphQLField(GraphQLString), + } + ) + + CatType = GraphQLObjectType( + name='Cat', + fields={ + 'name': GraphQLField(GraphQLString), + 'meows': GraphQLField(GraphQLBoolean) + } + ) + + PetType = GraphQLUnionType( + name='Pet', + types=[DogType, CatType], + resolve_type=make_type_resolver(lambda: [ + (Dog, DogType), + (Cat, CatType), + (Human, HumanType) + ]) + ) + + schema = GraphQLSchema( + query=GraphQLObjectType( + name='Query', + fields={ + 'pets': GraphQLField( + GraphQLList(PetType), + resolver=lambda *_: [Dog('Odie', True), Cat('Garfield', False), Human('Jon')] + ) + } + ) + ) + + query = ''' + { + pets { + ... on Dog { + name + woofs + } + ... on Cat { + name + meows + } + } + } + ''' + + result = graphql(schema, query) + assert result.errors[0].message == 'Runtime Object type "Human" is not a possible type for "Pet".' + assert result.data == {'pets': [{'woofs': True, 'name': 'Odie'}, {'name': 'Garfield', 'meows': False}, None]} diff --git a/graphql/execution/experimental/tests/test_directives.py b/graphql/execution/experimental/tests/test_directives.py new file mode 100644 index 00000000..9d8e4aaa --- /dev/null +++ b/graphql/execution/experimental/tests/test_directives.py @@ -0,0 +1,259 @@ +from graphql.language.parser import parse +from graphql.type import (GraphQLField, GraphQLObjectType, GraphQLSchema, + GraphQLString) + +from ..executor import execute + +schema = GraphQLSchema( + query=GraphQLObjectType( + name='TestType', + fields={ + 'a': GraphQLField(GraphQLString), + 'b': GraphQLField(GraphQLString), + } + ) +) + + +class Data(object): + a = 'a' + b = 'b' + + +def execute_test_query(doc): + return execute(schema, parse(doc), Data) + + +def test_basic_query_works(): + result = execute_test_query('{ a, b }') + assert not result.errors + assert result.data == {'a': 'a', 'b': 'b'} + + +def test_if_true_includes_scalar(): + result = execute_test_query('{ a, b @include(if: true) }') + assert not result.errors + assert result.data == {'a': 'a', 'b': 'b'} + + +def test_if_false_omits_on_scalar(): + result = execute_test_query('{ a, b @include(if: false) }') + assert not result.errors + assert result.data == {'a': 'a'} + + +def test_skip_false_includes_scalar(): + result = execute_test_query('{ a, b @skip(if: false) }') + assert not result.errors + assert result.data == {'a': 'a', 'b': 'b'} + + +def test_skip_true_omits_scalar(): + result = execute_test_query('{ a, b @skip(if: true) }') + assert not result.errors + assert result.data == {'a': 'a'} + + +def test_if_false_omits_fragment_spread(): + q = ''' + query Q { + a + ...Frag @include(if: false) + } + fragment Frag on TestType { + b + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a'} + + +def test_if_true_includes_fragment_spread(): + q = ''' + query Q { + a + ...Frag @include(if: true) + } + fragment Frag on TestType { + b + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a', 'b': 'b'} + + +def test_skip_false_includes_fragment_spread(): + q = ''' + query Q { + a + ...Frag @skip(if: false) + } + fragment Frag on TestType { + b + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a', 'b': 'b'} + + +def test_skip_true_omits_fragment_spread(): + q = ''' + query Q { + a + ...Frag @skip(if: true) + } + fragment Frag on TestType { + b + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a'} + + +def test_if_false_omits_inline_fragment(): + q = ''' + query Q { + a + ... on TestType @include(if: false) { + b + } + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a'} + + +def test_if_true_includes_inline_fragment(): + q = ''' + query Q { + a + ... on TestType @include(if: true) { + b + } + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a', 'b': 'b'} + + +def test_skip_false_includes_inline_fragment(): + q = ''' + query Q { + a + ... on TestType @skip(if: false) { + b + } + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a', 'b': 'b'} + + +def test_skip_true_omits_inline_fragment(): + q = ''' + query Q { + a + ... on TestType @skip(if: true) { + b + } + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a'} + + +def test_skip_true_omits_fragment(): + q = ''' + query Q { + a + ...Frag + } + fragment Frag on TestType @skip(if: true) { + b + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a'} + + +def test_skip_on_inline_anonymous_fragment_omits_field(): + q = ''' + query Q { + a + ... @skip(if: true) { + b + } + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a'} + + +def test_skip_on_inline_anonymous_fragment_does_not_omit_field(): + q = ''' + query Q { + a + ... @skip(if: false) { + b + } + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a', 'b': 'b'} + + +def test_include_on_inline_anonymous_fragment_omits_field(): + q = ''' + query Q { + a + ... @include(if: false) { + b + } + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a'} + + +def test_include_on_inline_anonymous_fragment_does_not_omit_field(): + q = ''' + query Q { + a + ... @include(if: true) { + b + } + } + ''' + result = execute_test_query(q) + assert not result.errors + assert result.data == {'a': 'a', 'b': 'b'} + + +def test_works_directives_include_and_no_skip(): + result = execute_test_query('{ a, b @include(if: true) @skip(if: false) }') + assert not result.errors + assert result.data == {'a': 'a', 'b': 'b'} + + +def test_works_directives_include_and_skip(): + result = execute_test_query('{ a, b @include(if: true) @skip(if: true) }') + assert not result.errors + assert result.data == {'a': 'a'} + + +def test_works_directives_no_include_or_skip(): + result = execute_test_query('{ a, b @include(if: false) @skip(if: false) }') + assert not result.errors + assert result.data == {'a': 'a'} diff --git a/graphql/execution/experimental/tests/test_executor.py b/graphql/execution/experimental/tests/test_executor.py new file mode 100644 index 00000000..7afe32a6 --- /dev/null +++ b/graphql/execution/experimental/tests/test_executor.py @@ -0,0 +1,180 @@ +from functools import partial + +import pytest + +from promise import Promise + +from ....language import ast +from ....language.parser import parse +from ....type import (GraphQLBoolean, GraphQLEnumType, GraphQLField, + GraphQLInt, GraphQLInterfaceType, GraphQLList, + GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, + GraphQLSchema, GraphQLString, GraphQLUnionType) +from ...base import ExecutionContext +from ..executor import execute +from ..fragment import Fragment +from ..resolver import type_resolver + + +# from ...executor import execute + + + +def test_fragment_resolver_abstract(benchmark): + all_slots = range(10000) + + Node = GraphQLInterfaceType('Node', fields={'id': GraphQLField(GraphQLInt)}) + Person = GraphQLObjectType('Person', interfaces=(Node, ), is_type_of=lambda *_, **__: True, fields={ + 'id': GraphQLField(GraphQLInt, resolver=lambda obj, *_, **__: obj), + 'name': GraphQLField(GraphQLString, resolver=lambda obj, *_, **__: "name:" + str(obj)) + }) + Query = GraphQLObjectType( + 'Query', + fields={ + 'nodes': GraphQLField( + GraphQLList(Node), + resolver=lambda *_, + **__: all_slots)}) + + document_ast = parse('''query { + nodes { + id + ... on Person { + name + } + } + }''') + # node_fragment = Fragment(type=Node, field_asts=node_field_asts) + schema = GraphQLSchema(query=Query, types=[Person]) + partial_execute = partial(execute, schema, document_ast) + resolved = benchmark(partial_execute) + # resolved = execute(schema, document_ast) + assert not resolved.errors + assert resolved.data == { + 'nodes': [{ + 'id': x, + 'name': 'name:' + str(x) + } for x in all_slots] + } + + +def test_fragment_resolver_context(): + Query = GraphQLObjectType('Query', fields={ + 'context': GraphQLField(GraphQLString, resolver=lambda root, args, context, info: context), + 'same_schema': GraphQLField(GraphQLBoolean, resolver=lambda root, args, context, info: info.schema == schema) + }) + + document_ast = parse('''query { + context + same_schema + }''') + # node_fragment = Fragment(type=Node, field_asts=node_field_asts) + schema = GraphQLSchema(query=Query) + # partial_execute = partial(execute, schema, document_ast, context_value="1") + # resolved = benchmark(partial_execute) + resolved = execute(schema, document_ast, context_value="1") + assert not resolved.errors + assert resolved.data == { + 'context': '1', + 'same_schema': True, + } + + +def test_fragment_resolver_fails(): + def raise_resolver(*args, **kwargs): + raise Exception("My exception") + + def succeeds_resolver(*args, **kwargs): + return True + + Query = GraphQLObjectType('Query', fields={ + 'fails': GraphQLField(GraphQLString, resolver=raise_resolver), + 'succeeds': GraphQLField(GraphQLBoolean, resolver=succeeds_resolver) + }) + + document_ast = parse('''query { + fails + succeeds + }''') + # node_fragment = Fragment(type=Node, field_asts=node_field_asts) + schema = GraphQLSchema(query=Query) + # partial_execute = partial(execute, schema, document_ast, context_value="1") + # resolved = benchmark(partial_execute) + resolved = execute(schema, document_ast, context_value="1") + assert len(resolved.errors) == 1 + assert resolved.data == { + 'fails': None, + 'succeeds': True, + } + + +def test_fragment_resolver_resolves_all_list(): + Query = GraphQLObjectType('Query', fields={ + 'ints': GraphQLField(GraphQLList(GraphQLNonNull(GraphQLInt)), resolver=lambda *args: [1, "2", "non"]), + }) + + document_ast = parse('''query { + ints + }''') + # node_fragment = Fragment(type=Node, field_asts=node_field_asts) + schema = GraphQLSchema(query=Query) + # partial_execute = partial(execute, schema, document_ast, context_value="1") + # resolved = benchmark(partial_execute) + resolved = execute(schema, document_ast) + assert len(resolved.errors) == 1 + assert str(resolved.errors[0]) == 'could not convert string to float: non' + assert resolved.data == { + 'ints': [1, 2, None] + } + + +def test_fragment_resolver_resolves_all_list(): + Person = GraphQLObjectType('Person', fields={ + 'id': GraphQLField(GraphQLInt, resolver=lambda obj, *_, **__: 1), + }) + Query = GraphQLObjectType('Query', fields={ + 'persons': GraphQLField(GraphQLList(GraphQLNonNull(Person)), resolver=lambda *args: [1, 2, None]), + }) + + document_ast = parse('''query { + persons { + id + } + }''') + # node_fragment = Fragment(type=Node, field_asts=node_field_asts) + schema = GraphQLSchema(query=Query, types=[Person]) + # partial_execute = partial(execute, schema, document_ast, context_value="1") + # resolved = benchmark(partial_execute) + resolved = execute(schema, document_ast) + assert len(resolved.errors) == 1 + assert str(resolved.errors[0]) == 'Cannot return null for non-nullable field Query.persons.' + # assert str(resolved.errors[0]) == 'could not convert string to float: non' + assert resolved.data == { + 'persons': None + } + + +def test_fragment_resolver_resolves_all_list_null(): + Person = GraphQLObjectType('Person', fields={ + 'id': GraphQLField(GraphQLInt, resolver=lambda obj, *_, **__: 1), + }) + Query = GraphQLObjectType('Query', fields={ + 'persons': GraphQLField(GraphQLList(GraphQLNonNull(Person)), resolver=lambda *args: [1, 2, None]), + }) + + document_ast = parse('''query { + persons { + id + } + }''') + # node_fragment = Fragment(type=Node, field_asts=node_field_asts) + schema = GraphQLSchema(query=Query, types=[Person]) + # partial_execute = partial(execute, schema, document_ast, context_value="1") + # resolved = benchmark(partial_execute) + resolved = execute(schema, document_ast) + assert len(resolved.errors) == 1 + assert str(resolved.errors[0]) == 'Cannot return null for non-nullable field Query.persons.' + # assert str(resolved.errors[0]) == 'could not convert string to float: non' + assert resolved.data == { + 'persons': None + } diff --git a/graphql/execution/experimental/tests/test_lists.py b/graphql/execution/experimental/tests/test_lists.py new file mode 100644 index 00000000..95079858 --- /dev/null +++ b/graphql/execution/experimental/tests/test_lists.py @@ -0,0 +1,211 @@ +from collections import namedtuple + +from graphql.error import format_error +from graphql.execution.experimental.executor import execute +from graphql.language.parser import parse +from graphql.type import (GraphQLField, GraphQLInt, GraphQLList, + GraphQLNonNull, GraphQLObjectType, GraphQLSchema) + +from .utils import rejected, resolved + +Data = namedtuple('Data', 'test') +ast = parse('{ nest { test } }') + + +def check(test_data, expected): + def run_check(self): + test_type = self.type + + data = Data(test=test_data) + DataType = GraphQLObjectType( + name='DataType', + fields=lambda: { + 'test': GraphQLField(test_type), + 'nest': GraphQLField(DataType, resolver=lambda *_: data) + } + ) + + schema = GraphQLSchema(query=DataType) + response = execute(schema, ast, data) + + if response.errors: + result = { + 'data': response.data, + 'errors': [format_error(e) for e in response.errors] + } + else: + result = { + 'data': response.data + } + + assert result == expected + + return run_check + + +class Test_ListOfT_Array_T: # [T] Array + type = GraphQLList(GraphQLInt) + + test_contains_values = check([1, 2], {'data': {'nest': {'test': [1, 2]}}}) + test_contains_null = check([1, None, 2], {'data': {'nest': {'test': [1, None, 2]}}}) + test_returns_null = check(None, {'data': {'nest': {'test': None}}}) + + +class Test_ListOfT_Promise_Array_T: # [T] Promise> + type = GraphQLList(GraphQLInt) + + test_contains_values = check(resolved([1, 2]), {'data': {'nest': {'test': [1, 2]}}}) + test_contains_null = check(resolved([1, None, 2]), {'data': {'nest': {'test': [1, None, 2]}}}) + test_returns_null = check(resolved(None), {'data': {'nest': {'test': None}}}) + test_rejected = check(lambda: rejected(Exception('bad')), { + 'data': {'nest': {'test': None}}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + }) + + +class Test_ListOfT_Array_Promise_T: # [T] Array> + type = GraphQLList(GraphQLInt) + + test_contains_values = check([resolved(1), resolved(2)], {'data': {'nest': {'test': [1, 2]}}}) + test_contains_null = check([resolved(1), resolved(None), resolved(2)], {'data': {'nest': {'test': [1, None, 2]}}}) + test_contains_reject = check(lambda: [resolved(1), rejected(Exception('bad')), resolved(2)], { + 'data': {'nest': {'test': [1, None, 2]}}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + }) + + +class Test_NotNullListOfT_Array_T: # [T]! Array + type = GraphQLNonNull(GraphQLList(GraphQLInt)) + + test_contains_values = check(resolved([1, 2]), {'data': {'nest': {'test': [1, 2]}}}) + test_contains_null = check(resolved([1, None, 2]), {'data': {'nest': {'test': [1, None, 2]}}}) + test_returns_null = check(resolved(None), { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'message': 'Cannot return null for non-nullable field DataType.test.'}] + }) + + +class Test_NotNullListOfT_Promise_Array_T: # [T]! Promise>> + type = GraphQLNonNull(GraphQLList(GraphQLInt)) + + test_contains_values = check(resolved([1, 2]), {'data': {'nest': {'test': [1, 2]}}}) + test_contains_null = check(resolved([1, None, 2]), {'data': {'nest': {'test': [1, None, 2]}}}) + test_returns_null = check(resolved(None), { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'message': 'Cannot return null for non-nullable field DataType.test.'}] + }) + + test_rejected = check(lambda: rejected(Exception('bad')), { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + }) + + +class Test_NotNullListOfT_Array_Promise_T: # [T]! Promise>> + type = GraphQLNonNull(GraphQLList(GraphQLInt)) + test_contains_values = check([resolved(1), resolved(2)], {'data': {'nest': {'test': [1, 2]}}}) + test_contains_null = check([resolved(1), resolved(None), resolved(2)], {'data': {'nest': {'test': [1, None, 2]}}}) + test_contains_reject = check(lambda: [resolved(1), rejected(Exception('bad')), resolved(2)], { + 'data': {'nest': {'test': [1, None, 2]}}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + }) + + +class TestListOfNotNullT_Array_T: # [T!] Array + type = GraphQLList(GraphQLNonNull(GraphQLInt)) + + test_contains_values = check([1, 2], {'data': {'nest': {'test': [1, 2]}}}) + test_contains_null = check([1, None, 2], { + 'data': {'nest': {'test': None}}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'message': 'Cannot return null for non-nullable field DataType.test.'}] + }) + test_returns_null = check(None, {'data': {'nest': {'test': None}}}) + + +class TestListOfNotNullT_Promise_Array_T: # [T!] Promise> + type = GraphQLList(GraphQLNonNull(GraphQLInt)) + + test_contains_value = check(resolved([1, 2]), {'data': {'nest': {'test': [1, 2]}}}) + test_contains_null = check(resolved([1, None, 2]), { + 'data': {'nest': {'test': None}}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'message': 'Cannot return null for non-nullable field DataType.test.'}] + }) + + test_returns_null = check(resolved(None), {'data': {'nest': {'test': None}}}) + + test_rejected = check(lambda: rejected(Exception('bad')), { + 'data': {'nest': {'test': None}}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + }) + + +class TestListOfNotNullT_Array_Promise_T: # [T!] Array> + type = GraphQLList(GraphQLNonNull(GraphQLInt)) + + test_contains_values = check([resolved(1), resolved(2)], {'data': {'nest': {'test': [1, 2]}}}) + test_contains_null = check([resolved(1), resolved(None), resolved(2)], { + 'data': {'nest': {'test': None}}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'message': 'Cannot return null for non-nullable field DataType.test.'}] + }) + test_contains_reject = check(lambda: [resolved(1), rejected(Exception('bad')), resolved(2)], { + 'data': {'nest': {'test': None}}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + }) + + +class TestNotNullListOfNotNullT_Array_T: # [T!]! Array + type = GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLInt))) + + test_contains_values = check([1, 2], {'data': {'nest': {'test': [1, 2]}}}) + test_contains_null = check([1, None, 2], { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'message': 'Cannot return null for non-nullable field DataType.test.'}] + }) + test_returns_null = check(None, { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'message': 'Cannot return null for non-nullable field DataType.test.'}] + }) + + +class TestNotNullListOfNotNullT_Promise_Array_T: # [T!]! Promise> + type = GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLInt))) + + test_contains_value = check(resolved([1, 2]), {'data': {'nest': {'test': [1, 2]}}}) + test_contains_null = check(resolved([1, None, 2]), { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'message': 'Cannot return null for non-nullable field DataType.test.'}] + }) + + test_returns_null = check(resolved(None), { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'message': 'Cannot return null for non-nullable field DataType.test.'}] + }) + + test_rejected = check(lambda: rejected(Exception('bad')), { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + }) + + +class TestNotNullListOfNotNullT_Array_Promise_T: # [T!]! Array> + type = GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLInt))) + + test_contains_values = check([resolved(1), resolved(2)], {'data': {'nest': {'test': [1, 2]}}}) + test_contains_null = check([resolved(1), resolved(None), resolved(2)], { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], + 'message': 'Cannot return null for non-nullable field DataType.test.'}] + }) + test_contains_reject = check(lambda: [resolved(1), rejected(Exception('bad')), resolved(2)], { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] + }) diff --git a/graphql/execution/experimental/tests/test_mutations.py b/graphql/execution/experimental/tests/test_mutations.py new file mode 100644 index 00000000..431073f4 --- /dev/null +++ b/graphql/execution/experimental/tests/test_mutations.py @@ -0,0 +1,140 @@ +from graphql.language.parser import parse +from graphql.type import (GraphQLArgument, GraphQLField, GraphQLInt, + GraphQLList, GraphQLObjectType, GraphQLSchema, + GraphQLString) + +from ..executor import execute + + +class NumberHolder(object): + + def __init__(self, n): + self.theNumber = n + + +class Root(object): + + def __init__(self, n): + self.numberHolder = NumberHolder(n) + + def immediately_change_the_number(self, n): + self.numberHolder.theNumber = n + return self.numberHolder + + def promise_to_change_the_number(self, n): + # TODO: async + return self.immediately_change_the_number(n) + + def fail_to_change_the_number(self, n): + raise Exception('Cannot change the number') + + def promise_and_fail_to_change_the_number(self, n): + # TODO: async + self.fail_to_change_the_number(n) + + +NumberHolderType = GraphQLObjectType('NumberHolder', { + 'theNumber': GraphQLField(GraphQLInt) +}) + +QueryType = GraphQLObjectType('Query', { + 'numberHolder': GraphQLField(NumberHolderType) +}) + +MutationType = GraphQLObjectType('Mutation', { + 'immediatelyChangeTheNumber': GraphQLField( + NumberHolderType, + args={'newNumber': GraphQLArgument(GraphQLInt)}, + resolver=lambda obj, args, *_: + obj.immediately_change_the_number(args['newNumber'])), + 'promiseToChangeTheNumber': GraphQLField( + NumberHolderType, + args={'newNumber': GraphQLArgument(GraphQLInt)}, + resolver=lambda obj, args, *_: + obj.promise_to_change_the_number(args['newNumber'])), + 'failToChangeTheNumber': GraphQLField( + NumberHolderType, + args={'newNumber': GraphQLArgument(GraphQLInt)}, + resolver=lambda obj, args, *_: + obj.fail_to_change_the_number(args['newNumber'])), + 'promiseAndFailToChangeTheNumber': GraphQLField( + NumberHolderType, + args={'newNumber': GraphQLArgument(GraphQLInt)}, + resolver=lambda obj, args, *_: + obj.promise_and_fail_to_change_the_number(args['newNumber'])), +}) + +schema = GraphQLSchema(QueryType, MutationType) + + +def assert_evaluate_mutations_serially(executor=None): + doc = '''mutation M { + first: immediatelyChangeTheNumber(newNumber: 1) { + theNumber + }, + second: promiseToChangeTheNumber(newNumber: 2) { + theNumber + }, + third: immediatelyChangeTheNumber(newNumber: 3) { + theNumber + } + fourth: promiseToChangeTheNumber(newNumber: 4) { + theNumber + }, + fifth: immediatelyChangeTheNumber(newNumber: 5) { + theNumber + } + }''' + ast = parse(doc) + result = execute(schema, ast, Root(6), operation_name='M', executor=executor) + assert not result.errors + assert result.data == \ + { + 'first': {'theNumber': 1}, + 'second': {'theNumber': 2}, + 'third': {'theNumber': 3}, + 'fourth': {'theNumber': 4}, + 'fifth': {'theNumber': 5}, + } + + +def test_evaluates_mutations_serially(): + assert_evaluate_mutations_serially() + + +def test_evaluates_mutations_correctly_in_the_presense_of_a_failed_mutation(): + doc = '''mutation M { + first: immediatelyChangeTheNumber(newNumber: 1) { + theNumber + }, + second: promiseToChangeTheNumber(newNumber: 2) { + theNumber + }, + third: failToChangeTheNumber(newNumber: 3) { + theNumber + } + fourth: promiseToChangeTheNumber(newNumber: 4) { + theNumber + }, + fifth: immediatelyChangeTheNumber(newNumber: 5) { + theNumber + } + sixth: promiseAndFailToChangeTheNumber(newNumber: 6) { + theNumber + } + }''' + ast = parse(doc) + result = execute(schema, ast, Root(6), operation_name='M') + assert result.data == \ + { + 'first': {'theNumber': 1}, + 'second': {'theNumber': 2}, + 'third': None, + 'fourth': {'theNumber': 4}, + 'fifth': {'theNumber': 5}, + 'sixth': None, + } + assert len(result.errors) == 2 + # TODO: check error location + assert result.errors[0].message == 'Cannot change the number' + assert result.errors[1].message == 'Cannot change the number' diff --git a/graphql/execution/experimental/tests/test_nonnull.py b/graphql/execution/experimental/tests/test_nonnull.py new file mode 100644 index 00000000..3a40b94d --- /dev/null +++ b/graphql/execution/experimental/tests/test_nonnull.py @@ -0,0 +1,585 @@ + +from graphql.error import format_error +from graphql.execution.experimental.executor import execute +from graphql.language.parser import parse +from graphql.type import (GraphQLField, GraphQLNonNull, GraphQLObjectType, + GraphQLSchema, GraphQLString) + +from .utils import rejected, resolved + +sync_error = Exception('sync') +non_null_sync_error = Exception('nonNullSync') +promise_error = Exception('promise') +non_null_promise_error = Exception('nonNullPromise') + + +class ThrowingData(object): + + def sync(self): + raise sync_error + + def nonNullSync(self): + raise non_null_sync_error + + def promise(self): + return rejected(promise_error) + + def nonNullPromise(self): + return rejected(non_null_promise_error) + + def nest(self): + return ThrowingData() + + def nonNullNest(self): + return ThrowingData() + + def promiseNest(self): + return resolved(ThrowingData()) + + def nonNullPromiseNest(self): + return resolved(ThrowingData()) + + +class NullingData(object): + + def sync(self): + return None + + def nonNullSync(self): + return None + + def promise(self): + return resolved(None) + + def nonNullPromise(self): + return resolved(None) + + def nest(self): + return NullingData() + + def nonNullNest(self): + return NullingData() + + def promiseNest(self): + return resolved(NullingData()) + + def nonNullPromiseNest(self): + return resolved(NullingData()) + + +DataType = GraphQLObjectType('DataType', lambda: { + 'sync': GraphQLField(GraphQLString), + 'nonNullSync': GraphQLField(GraphQLNonNull(GraphQLString)), + 'promise': GraphQLField(GraphQLString), + 'nonNullPromise': GraphQLField(GraphQLNonNull(GraphQLString)), + 'nest': GraphQLField(DataType), + 'nonNullNest': GraphQLField(GraphQLNonNull(DataType)), + 'promiseNest': GraphQLField(DataType), + 'nonNullPromiseNest': GraphQLField(GraphQLNonNull(DataType)) +}) + +schema = GraphQLSchema(DataType) + + +def order_errors(error): + locations = error['locations'] + return (locations[0]['column'], locations[0]['line']) + + +def check(doc, data, expected): + ast = parse(doc) + response = execute(schema, ast, data) + + if response.errors: + result = { + 'data': response.data, + 'errors': [format_error(e) for e in response.errors] + } + if result['errors'] != expected['errors']: + assert result['data'] == expected['data'] + # Sometimes the fields resolves asynchronously, so + # we need to check that the errors are the same, but might be + # raised in a different order. + assert sorted(result['errors'], key=order_errors) == sorted(expected['errors'], key=order_errors) + else: + assert result == expected + else: + result = { + 'data': response.data + } + + assert result == expected + + +def test_nulls_a_nullable_field_that_throws_sync(): + doc = ''' + query Q { + sync + } + ''' + + check(doc, ThrowingData(), { + 'data': {'sync': None}, + 'errors': [{'locations': [{'column': 13, 'line': 3}], 'message': str(sync_error)}] + }) + + +def test_nulls_a_nullable_field_that_throws_in_a_promise(): + doc = ''' + query Q { + promise + } + ''' + + check(doc, ThrowingData(), { + 'data': {'promise': None}, + 'errors': [{'locations': [{'column': 13, 'line': 3}], 'message': str(promise_error)}] + }) + + +def test_nulls_a_sync_returned_object_that_contains_a_non_nullable_field_that_throws(): + doc = ''' + query Q { + nest { + nonNullSync, + } + } + ''' + + check(doc, ThrowingData(), { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'message': str(non_null_sync_error)}] + }) + + +def test_nulls_a_synchronously_returned_object_that_contains_a_non_nullable_field_that_throws_in_a_promise(): + doc = ''' + query Q { + nest { + nonNullPromise, + } + } + ''' + + check(doc, ThrowingData(), { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'message': str(non_null_promise_error)}] + }) + + +def test_nulls_an_object_returned_in_a_promise_that_contains_a_non_nullable_field_that_throws_synchronously(): + doc = ''' + query Q { + promiseNest { + nonNullSync, + } + } + ''' + + check(doc, ThrowingData(), { + 'data': {'promiseNest': None}, + 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'message': str(non_null_sync_error)}] + }) + + +def test_nulls_an_object_returned_in_a_promise_that_contains_a_non_nullable_field_that_throws_in_a_promise(): + doc = ''' + query Q { + promiseNest { + nonNullPromise, + } + } + ''' + + check(doc, ThrowingData(), { + 'data': {'promiseNest': None}, + 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'message': str(non_null_promise_error)}] + }) + + +def test_nulls_a_complex_tree_of_nullable_fields_that_throw(): + doc = ''' + query Q { + nest { + sync + promise + nest { + sync + promise + } + promiseNest { + sync + promise + } + } + promiseNest { + sync + promise + nest { + sync + promise + } + promiseNest { + sync + promise + } + } + } + ''' + check(doc, ThrowingData(), { + 'data': {'nest': {'nest': {'promise': None, 'sync': None}, + 'promise': None, + 'promiseNest': {'promise': None, 'sync': None}, + 'sync': None}, + 'promiseNest': {'nest': {'promise': None, 'sync': None}, + 'promise': None, + 'promiseNest': {'promise': None, 'sync': None}, + 'sync': None}}, + 'errors': [{'locations': [{'column': 11, 'line': 4}], 'message': str(sync_error)}, + {'locations': [{'column': 11, 'line': 5}], 'message': str(promise_error)}, + {'locations': [{'column': 13, 'line': 7}], 'message': str(sync_error)}, + {'locations': [{'column': 13, 'line': 8}], 'message': str(promise_error)}, + {'locations': [{'column': 13, 'line': 11}], 'message': str(sync_error)}, + {'locations': [{'column': 13, 'line': 12}], 'message': str(promise_error)}, + {'locations': [{'column': 11, 'line': 16}], 'message': str(sync_error)}, + {'locations': [{'column': 11, 'line': 17}], 'message': str(promise_error)}, + {'locations': [{'column': 13, 'line': 19}], 'message': str(sync_error)}, + {'locations': [{'column': 13, 'line': 20}], 'message': str(promise_error)}, + {'locations': [{'column': 13, 'line': 23}], 'message': str(sync_error)}, + {'locations': [{'column': 13, 'line': 24}], 'message': str(promise_error)}] + }) + + +def test_nulls_the_first_nullable_object_after_a_field_throws_in_a_long_chain_of_fields_that_are_non_null(): + doc = ''' + query Q { + nest { + nonNullNest { + nonNullPromiseNest { + nonNullNest { + nonNullPromiseNest { + nonNullSync + } + } + } + } + } + promiseNest { + nonNullNest { + nonNullPromiseNest { + nonNullNest { + nonNullPromiseNest { + nonNullSync + } + } + } + } + } + anotherNest: nest { + nonNullNest { + nonNullPromiseNest { + nonNullNest { + nonNullPromiseNest { + nonNullPromise + } + } + } + } + } + anotherPromiseNest: promiseNest { + nonNullNest { + nonNullPromiseNest { + nonNullNest { + nonNullPromiseNest { + nonNullPromise + } + } + } + } + } + } + ''' + check(doc, ThrowingData(), { + 'data': {'nest': None, 'promiseNest': None, 'anotherNest': None, 'anotherPromiseNest': None}, + 'errors': [{'locations': [{'column': 19, 'line': 8}], + 'message': str(non_null_sync_error)}, + {'locations': [{'column': 19, 'line': 19}], + 'message': str(non_null_sync_error)}, + {'locations': [{'column': 19, 'line': 30}], + 'message': str(non_null_promise_error)}, + {'locations': [{'column': 19, 'line': 41}], + 'message': str(non_null_promise_error)}] + }) + + +def test_nulls_a_nullable_field_that_returns_null(): + doc = ''' + query Q { + sync + } + ''' + + check(doc, NullingData(), { + 'data': {'sync': None} + }) + + +def test_nulls_a_nullable_field_that_returns_null_in_a_promise(): + doc = ''' + query Q { + promise + } + ''' + + check(doc, NullingData(), { + 'data': {'promise': None} + }) + + +def test_nulls_a_sync_returned_object_that_contains_a_non_nullable_field_that_returns_null_synchronously(): + doc = ''' + query Q { + nest { + nonNullSync, + } + } + ''' + check(doc, NullingData(), { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'message': 'Cannot return null for non-nullable field DataType.nonNullSync.'}] + }) + + +def test_nulls_a_synchronously_returned_object_that_contains_a_non_nullable_field_that_returns_null_in_a_promise(): + doc = ''' + query Q { + nest { + nonNullPromise, + } + } + ''' + check(doc, NullingData(), { + 'data': {'nest': None}, + 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'message': 'Cannot return null for non-nullable field DataType.nonNullPromise.'}] + }) + + +def test_nulls_an_object_returned_in_a_promise_that_contains_a_non_nullable_field_that_returns_null_synchronously(): + doc = ''' + query Q { + promiseNest { + nonNullSync, + } + } + ''' + check(doc, NullingData(), { + 'data': {'promiseNest': None}, + 'errors': [{'locations': [{'column': 17, 'line': 4}], + 'message': 'Cannot return null for non-nullable field DataType.nonNullSync.'}] + }) + + +def test_nulls_an_object_returned_in_a_promise_that_contains_a_non_nullable_field_that_returns_null_ina_a_promise(): + doc = ''' + query Q { + promiseNest { + nonNullPromise + } + } + ''' + + check(doc, NullingData(), { + 'data': {'promiseNest': None}, + 'errors': [ + {'locations': [{'column': 17, 'line': 4}], + 'message': 'Cannot return null for non-nullable field DataType.nonNullPromise.'} + ] + }) + + +def test_nulls_a_complex_tree_of_nullable_fields_that_returns_null(): + doc = ''' + query Q { + nest { + sync + promise + nest { + sync + promise + } + promiseNest { + sync + promise + } + } + promiseNest { + sync + promise + nest { + sync + promise + } + promiseNest { + sync + promise + } + } + } + ''' + check(doc, NullingData(), { + 'data': { + 'nest': { + 'sync': None, + 'promise': None, + 'nest': { + 'sync': None, + 'promise': None, + }, + 'promiseNest': { + 'sync': None, + 'promise': None, + } + }, + 'promiseNest': { + 'sync': None, + 'promise': None, + 'nest': { + 'sync': None, + 'promise': None, + }, + 'promiseNest': { + 'sync': None, + 'promise': None, + } + } + } + }) + + +def test_nulls_the_first_nullable_object_after_a_field_returns_null_in_a_long_chain_of_fields_that_are_non_null(): + doc = ''' + query Q { + nest { + nonNullNest { + nonNullPromiseNest { + nonNullNest { + nonNullPromiseNest { + nonNullSync + } + } + } + } + } + promiseNest { + nonNullNest { + nonNullPromiseNest { + nonNullNest { + nonNullPromiseNest { + nonNullSync + } + } + } + } + } + anotherNest: nest { + nonNullNest { + nonNullPromiseNest { + nonNullNest { + nonNullPromiseNest { + nonNullPromise + } + } + } + } + } + anotherPromiseNest: promiseNest { + nonNullNest { + nonNullPromiseNest { + nonNullNest { + nonNullPromiseNest { + nonNullPromise + } + } + } + } + } + } + ''' + + check(doc, NullingData(), { + 'data': { + 'nest': None, + 'promiseNest': None, + 'anotherNest': None, + 'anotherPromiseNest': None + }, + 'errors': [ + {'locations': [{'column': 19, 'line': 8}], + 'message': 'Cannot return null for non-nullable field DataType.nonNullSync.'}, + {'locations': [{'column': 19, 'line': 19}], + 'message': 'Cannot return null for non-nullable field DataType.nonNullSync.'}, + {'locations': [{'column': 19, 'line': 30}], + 'message': 'Cannot return null for non-nullable field DataType.nonNullPromise.'}, + {'locations': [{'column': 19, 'line': 41}], + 'message': 'Cannot return null for non-nullable field DataType.nonNullPromise.'} + ] + }) + + +def test_nulls_the_top_level_if_sync_non_nullable_field_throws(): + doc = ''' + query Q { nonNullSync } + ''' + check(doc, ThrowingData(), { + 'data': None, + 'errors': [ + {'locations': [{'column': 19, 'line': 2}], + 'message': str(non_null_sync_error)} + ] + }) + + +def test_nulls_the_top_level_if_async_non_nullable_field_errors(): + doc = ''' + query Q { nonNullPromise } + ''' + + check(doc, ThrowingData(), { + 'data': None, + 'errors': [ + {'locations': [{'column': 19, 'line': 2}], + 'message': str(non_null_promise_error)} + ] + }) + + +def test_nulls_the_top_level_if_sync_non_nullable_field_returns_null(): + doc = ''' + query Q { nonNullSync } + ''' + check(doc, NullingData(), { + 'data': None, + 'errors': [ + {'locations': [{'column': 19, 'line': 2}], + 'message': 'Cannot return null for non-nullable field DataType.nonNullSync.'} + ] + }) + + +def test_nulls_the_top_level_if_async_non_nullable_field_resolves_null(): + doc = ''' + query Q { nonNullPromise } + ''' + check(doc, NullingData(), { + 'data': None, + 'errors': [ + {'locations': [{'column': 19, 'line': 2}], + 'message': 'Cannot return null for non-nullable field DataType.nonNullPromise.'} + ] + }) diff --git a/graphql/execution/experimental/tests/test_querybuilder.py b/graphql/execution/experimental/tests/test_querybuilder.py new file mode 100644 index 00000000..e1c47b2a --- /dev/null +++ b/graphql/execution/experimental/tests/test_querybuilder.py @@ -0,0 +1,168 @@ +# from ..experimental import generate_fragment, fragment_operation, experimental +# from ..fragment import Fragment + +# from ....language.parser import parse +# from ....language import ast +# from ....type import (GraphQLEnumType, GraphQLInterfaceType, GraphQLList, +# GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, +# GraphQLSchema, GraphQLUnionType, GraphQLString, GraphQLInt, GraphQLField) + + +# def test_generate_fragment(): +# Node = GraphQLObjectType('Node', fields={'id': GraphQLField(GraphQLInt)}) +# Query = GraphQLObjectType('Query', fields={'nodes': GraphQLField(GraphQLList(Node))}) +# node_field_asts = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='id'), +# ) +# ]) +# query_field_asts = [ +# ast.Field( +# name=ast.Name(value='nodes'), +# selection_set=node_field_asts +# ) +# ] +# QueryFragment = generate_fragment(Query, query_field_asts) + +# assert QueryFragment == Fragment( +# Query, +# query_field_asts, +# field_fragments={ +# 'nodes': Fragment( +# Node, +# node_field_asts.selections +# ) +# }, +# ) + + +# def test_fragment_operation_query(): +# Node = GraphQLObjectType('Node', fields={'id': GraphQLField(GraphQLInt)}) +# Query = GraphQLObjectType('Query', fields={'nodes': GraphQLField(GraphQLList(Node))}) + +# schema = GraphQLSchema(query=Query) + +# node_field_asts = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='id'), +# ) +# ]) +# query_field_asts = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='nodes'), +# selection_set=node_field_asts +# ) +# ]) +# operation_ast = ast.OperationDefinition( +# operation='query', +# selection_set=query_field_asts +# ) +# QueryFragment = fragment_operation(schema, operation_ast) + +# assert QueryFragment == Fragment( +# Query, +# query_field_asts.selections, +# field_fragments={ +# 'nodes': Fragment( +# Node, +# node_field_asts.selections +# ) +# }, +# ) + + +# def test_fragment_operation_mutation(): +# Node = GraphQLObjectType('Node', fields={'id': GraphQLField(GraphQLInt)}) +# Query = GraphQLObjectType('Query', fields={'nodes': GraphQLField(GraphQLList(Node))}) + +# schema = GraphQLSchema(query=Query, mutation=Query) + +# node_field_asts = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='id'), +# ) +# ]) +# query_field_asts = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='nodes'), +# selection_set=node_field_asts +# ) +# ]) +# operation_ast = ast.OperationDefinition( +# operation='mutation', +# selection_set=query_field_asts +# ) +# MutationFragment = fragment_operation(schema, operation_ast) + +# assert MutationFragment == Fragment( +# Query, +# query_field_asts.selections, +# field_fragments={ +# 'nodes': Fragment( +# Node, +# node_field_asts.selections +# ) +# }, +# execute_serially=True +# ) + + +# def test_query_builder_operation(): +# Node = GraphQLObjectType('Node', fields={'id': GraphQLField(GraphQLInt)}) +# Query = GraphQLObjectType('Query', fields={'nodes': GraphQLField(GraphQLList(Node))}) + +# schema = GraphQLSchema(query=Query, mutation=Query) +# document_ast = parse('''query MyQuery { +# nodes { +# id +# } +# }''') +# query_builder = experimental(schema, document_ast) +# QueryFragment = query_builder.get_operation_fragment('MyQuery') +# node_field_asts = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='id'), +# arguments=[], +# directives=[], +# selection_set=None, +# ) +# ]) +# query_field_asts = ast.SelectionSet(selections=[ +# ast.Field( +# name=ast.Name(value='nodes'), +# arguments=[], +# directives=[], +# selection_set=node_field_asts +# ) +# ]) +# assert QueryFragment == Fragment( +# Query, +# query_field_asts.selections, +# field_fragments={ +# 'nodes': Fragment( +# Node, +# node_field_asts.selections +# ) +# } +# ) + + +# def test_query_builder_execution(): +# Node = GraphQLObjectType('Node', fields={'id': GraphQLField(GraphQLInt, resolver=lambda obj, **__: obj)}) +# Query = GraphQLObjectType('Query', fields={'nodes': GraphQLField(GraphQLList(Node), resolver=lambda *_, **__: range(3))}) + +# schema = GraphQLSchema(query=Query) +# document_ast = parse('''query MyQuery { +# nodes { +# id +# } +# }''') +# query_builder = experimental(schema, document_ast) +# QueryFragment = query_builder.get_operation_fragment('MyQuery') +# root = None +# expected = { +# 'nodes': [{ +# 'id': n +# } for n in range(3)] +# } +# assert QueryFragment.resolver(lambda: root) == expected diff --git a/graphql/execution/experimental/tests/test_resolve.py b/graphql/execution/experimental/tests/test_resolve.py new file mode 100644 index 00000000..ef688003 --- /dev/null +++ b/graphql/execution/experimental/tests/test_resolve.py @@ -0,0 +1,136 @@ +import json +from collections import OrderedDict + +from graphql.type import (GraphQLArgument, GraphQLField, + GraphQLInputObjectField, GraphQLInputObjectType, + GraphQLInt, GraphQLList, GraphQLNonNull, + GraphQLObjectType, GraphQLSchema, GraphQLString) + +from ..executor import execute +from .utils import graphql + + +def _test_schema(test_field): + return GraphQLSchema( + query=GraphQLObjectType( + name='Query', + fields={ + 'test': test_field + } + ) + ) + + +def test_default_function_accesses_properties(): + schema = _test_schema(GraphQLField(GraphQLString)) + + class source: + test = 'testValue' + + result = graphql(schema, '{ test }', source) + assert not result.errors + assert result.data == {'test': 'testValue'} + + +def test_default_function_calls_methods(): + schema = _test_schema(GraphQLField(GraphQLString)) + + class source: + _secret = 'testValue' + + def test(self): + return self._secret + + result = graphql(schema, '{ test }', source()) + assert not result.errors + assert result.data == {'test': 'testValue'} + + +def test_uses_provided_resolve_function(): + def resolver(source, args, *_): + return json.dumps([source, args], separators=(',', ':')) + + schema = _test_schema(GraphQLField( + GraphQLString, + args=OrderedDict([ + ('aStr', GraphQLArgument(GraphQLString)), + ('aInt', GraphQLArgument(GraphQLInt)), + ]), + resolver=resolver + )) + + result = graphql(schema, '{ test }', None) + assert not result.errors + assert result.data == {'test': '[null,{}]'} + + result = graphql(schema, '{ test(aStr: "String!") }', 'Source!') + assert not result.errors + assert result.data == {'test': '["Source!",{"aStr":"String!"}]'} + + result = graphql(schema, '{ test(aInt: -123, aStr: "String!",) }', 'Source!') + assert not result.errors + assert result.data in [ + {'test': '["Source!",{"aStr":"String!","aInt":-123}]'}, + {'test': '["Source!",{"aInt":-123,"aStr":"String!"}]'} + ] + + +def test_maps_argument_out_names_well(): + def resolver(source, args, *_): + return json.dumps([source, args], separators=(',', ':')) + + schema = _test_schema(GraphQLField( + GraphQLString, + args=OrderedDict([ + ('aStr', GraphQLArgument(GraphQLString, out_name="a_str")), + ('aInt', GraphQLArgument(GraphQLInt, out_name="a_int")), + ]), + resolver=resolver + )) + + result = graphql(schema, '{ test }', None) + assert not result.errors + assert result.data == {'test': '[null,{}]'} + + result = graphql(schema, '{ test(aStr: "String!") }', 'Source!') + assert not result.errors + assert result.data == {'test': '["Source!",{"a_str":"String!"}]'} + + result = graphql(schema, '{ test(aInt: -123, aStr: "String!",) }', 'Source!') + assert not result.errors + assert result.data in [ + {'test': '["Source!",{"a_str":"String!","a_int":-123}]'}, + {'test': '["Source!",{"a_int":-123,"a_str":"String!"}]'} + ] + + +def test_maps_argument_out_names_well_with_input(): + def resolver(source, args, *_): + return json.dumps([source, args], separators=(',', ':')) + + TestInputObject = GraphQLInputObjectType('TestInputObject', lambda: OrderedDict([ + ('inputOne', GraphQLInputObjectField(GraphQLString, out_name="input_one")), + ('inputRecursive', GraphQLInputObjectField(TestInputObject, out_name="input_recursive")), + ])) + + schema = _test_schema(GraphQLField( + GraphQLString, + args=OrderedDict([ + ('aInput', GraphQLArgument(TestInputObject, out_name="a_input")) + ]), + resolver=resolver + )) + + result = graphql(schema, '{ test }', None) + assert not result.errors + assert result.data == {'test': '[null,{}]'} + + result = graphql(schema, '{ test(aInput: {inputOne: "String!"} ) }', 'Source!') + assert not result.errors + assert result.data == {'test': '["Source!",{"a_input":{"input_one":"String!"}}]'} + + result = graphql(schema, '{ test(aInput: {inputRecursive:{inputOne: "SourceRecursive!"}} ) }', 'Source!') + assert not result.errors + assert result.data == { + 'test': '["Source!",{"a_input":{"input_recursive":{"input_one":"SourceRecursive!"}}}]' + } diff --git a/graphql/execution/experimental/tests/test_union_interface.py b/graphql/execution/experimental/tests/test_union_interface.py new file mode 100644 index 00000000..2d91ebc4 --- /dev/null +++ b/graphql/execution/experimental/tests/test_union_interface.py @@ -0,0 +1,358 @@ +from graphql.language.parser import parse +from graphql.type import (GraphQLBoolean, GraphQLField, GraphQLInterfaceType, + GraphQLList, GraphQLObjectType, GraphQLSchema, + GraphQLString, GraphQLUnionType) + +from ..executor import execute + + +class Dog(object): + + def __init__(self, name, barks): + self.name = name + self.barks = barks + + +class Cat(object): + + def __init__(self, name, meows): + self.name = name + self.meows = meows + + +class Person(object): + + def __init__(self, name, pets, friends): + self.name = name + self.pets = pets + self.friends = friends + + +NamedType = GraphQLInterfaceType('Named', { + 'name': GraphQLField(GraphQLString) +}) + +DogType = GraphQLObjectType( + name='Dog', + interfaces=[NamedType], + fields={ + 'name': GraphQLField(GraphQLString), + 'barks': GraphQLField(GraphQLBoolean), + }, + is_type_of=lambda value, context, info: isinstance(value, Dog) +) + +CatType = GraphQLObjectType( + name='Cat', + interfaces=[NamedType], + fields={ + 'name': GraphQLField(GraphQLString), + 'meows': GraphQLField(GraphQLBoolean), + }, + is_type_of=lambda value, context, info: isinstance(value, Cat) +) + + +def resolve_pet_type(value, context, info): + if isinstance(value, Dog): + return DogType + if isinstance(value, Cat): + return CatType + + +PetType = GraphQLUnionType('Pet', [DogType, CatType], + resolve_type=resolve_pet_type) + +PersonType = GraphQLObjectType( + name='Person', + interfaces=[NamedType], + fields={ + 'name': GraphQLField(GraphQLString), + 'pets': GraphQLField(GraphQLList(PetType)), + 'friends': GraphQLField(GraphQLList(NamedType)), + }, + is_type_of=lambda value, context, info: isinstance(value, Person) +) + +schema = GraphQLSchema(query=PersonType, types=[PetType]) + +garfield = Cat('Garfield', False) +odie = Dog('Odie', True) +liz = Person('Liz', [], []) +john = Person('John', [garfield, odie], [liz, odie]) + + +# Execute: Union and intersection types + +def test_can_introspect_on_union_and_intersection_types(): + ast = parse(''' + { + Named: __type(name: "Named") { + kind + name + fields { name } + interfaces { name } + possibleTypes { name } + enumValues { name } + inputFields { name } + } + Pet: __type(name: "Pet") { + kind + name + fields { name } + interfaces { name } + possibleTypes { name } + enumValues { name } + inputFields { name } + } + }''') + + result = execute(schema, ast) + assert not result.errors + assert result.data == { + 'Named': { + 'enumValues': None, + 'name': 'Named', + 'kind': 'INTERFACE', + 'interfaces': None, + 'fields': [{'name': 'name'}], + 'possibleTypes': [{'name': 'Person'}, {'name': 'Dog'}, {'name': 'Cat'}], + 'inputFields': None + }, + 'Pet': { + 'enumValues': None, + 'name': 'Pet', + 'kind': 'UNION', + 'interfaces': None, + 'fields': None, + 'possibleTypes': [{'name': 'Dog'}, {'name': 'Cat'}], + 'inputFields': None + } + } + + +def test_executes_using_union_types(): + # NOTE: This is an *invalid* query, but it should be an *executable* query. + ast = parse(''' + { + __typename + name + pets { + __typename + name + barks + meows + } + } + ''') + result = execute(schema, ast, john) + + assert not result.errors + assert result.data == { + '__typename': 'Person', + 'name': 'John', + 'pets': [ + {'__typename': 'Cat', 'name': 'Garfield', 'meows': False}, + {'__typename': 'Dog', 'name': 'Odie', 'barks': True} + ] + } + + +def test_executes_union_types_with_inline_fragment(): + # This is the valid version of the query in the above test. + ast = parse(''' + { + __typename + name + pets { + __typename + ... on Dog { + name + barks + } + ... on Cat { + name + meows + } + } + } + ''') + result = execute(schema, ast, john) + assert not result.errors + assert result.data == { + '__typename': 'Person', + 'name': 'John', + 'pets': [ + {'__typename': 'Cat', 'name': 'Garfield', 'meows': False}, + {'__typename': 'Dog', 'name': 'Odie', 'barks': True} + ] + } + + +def test_executes_using_interface_types(): + # NOTE: This is an *invalid* query, but it should be an *executable* query. + ast = parse(''' + { + __typename + name + friends { + __typename + name + barks + meows + } + } + ''') + result = execute(schema, ast, john) + assert not result.errors + assert result.data == { + '__typename': 'Person', + 'name': 'John', + 'friends': [ + {'__typename': 'Person', 'name': 'Liz'}, + {'__typename': 'Dog', 'name': 'Odie', 'barks': True} + ] + } + + +def test_executes_interface_types_with_inline_fragment(): + # This is the valid version of the query in the above test. + ast = parse(''' + { + __typename + name + friends { + __typename + name + ... on Dog { + barks + } + ... on Cat { + meows + } + } + } + ''') + result = execute(schema, ast, john) + assert not result.errors + assert result.data == { + '__typename': 'Person', + 'name': 'John', + 'friends': [ + {'__typename': 'Person', 'name': 'Liz'}, + {'__typename': 'Dog', 'name': 'Odie', 'barks': True} + ] + } + + +def test_allows_fragment_conditions_to_be_abstract_types(): + ast = parse(''' + { + __typename + name + pets { ...PetFields } + friends { ...FriendFields } + } + fragment PetFields on Pet { + __typename + ... on Dog { + name + barks + } + ... on Cat { + name + meows + } + } + fragment FriendFields on Named { + __typename + name + ... on Dog { + barks + } + ... on Cat { + meows + } + } + ''') + result = execute(schema, ast, john) + assert not result.errors + assert result.data == { + '__typename': 'Person', + 'name': 'John', + 'pets': [ + {'__typename': 'Cat', 'name': 'Garfield', 'meows': False}, + {'__typename': 'Dog', 'name': 'Odie', 'barks': True} + ], + 'friends': [ + {'__typename': 'Person', 'name': 'Liz'}, + {'__typename': 'Dog', 'name': 'Odie', 'barks': True} + ] + } + + +def test_only_include_fields_from_matching_fragment_condition(): + ast = parse(''' + { + pets { ...PetFields } + } + fragment PetFields on Pet { + __typename + ... on Dog { + name + } + } + ''') + result = execute(schema, ast, john) + assert not result.errors + assert result.data == { + 'pets': [ + {'__typename': 'Cat'}, + {'__typename': 'Dog', 'name': 'Odie'} + ], + } + + +def test_gets_execution_info_in_resolver(): + class encountered: + schema = None + root_value = None + context = None + + def resolve_type(obj, context, info): + encountered.schema = info.schema + encountered.root_value = info.root_value + encountered.context = context + return PersonType2 + + NamedType2 = GraphQLInterfaceType( + name='Named', + fields={ + 'name': GraphQLField(GraphQLString) + }, + resolve_type=resolve_type + ) + + PersonType2 = GraphQLObjectType( + name='Person', + interfaces=[NamedType2], + fields={ + 'name': GraphQLField(GraphQLString), + 'friends': GraphQLField(GraphQLList(NamedType2)) + } + ) + + schema2 = GraphQLSchema(query=PersonType2) + john2 = Person('John', [], [liz]) + context = {'hey'} + ast = parse('''{ name, friends { name } }''') + + result = execute(schema2, ast, john2, context_value=context) + assert not result.errors + assert result.data == { + 'name': 'John', 'friends': [{'name': 'Liz'}] + } + + assert encountered.schema == schema2 + assert encountered.root_value == john2 + assert encountered.context == context diff --git a/graphql/execution/experimental/tests/test_variables.py b/graphql/execution/experimental/tests/test_variables.py new file mode 100644 index 00000000..83e91a22 --- /dev/null +++ b/graphql/execution/experimental/tests/test_variables.py @@ -0,0 +1,667 @@ +import json +from collections import OrderedDict + +from pytest import raises + +from graphql.error import GraphQLError, format_error +from graphql.execution import execute +from graphql.language.parser import parse +from graphql.type import (GraphQLArgument, GraphQLField, + GraphQLInputObjectField, GraphQLInputObjectType, + GraphQLList, GraphQLNonNull, GraphQLObjectType, + GraphQLScalarType, GraphQLSchema, GraphQLString) + +TestComplexScalar = GraphQLScalarType( + name='ComplexScalar', + serialize=lambda v: 'SerializedValue' if v == 'DeserializedValue' else None, + parse_value=lambda v: 'DeserializedValue' if v == 'SerializedValue' else None, + parse_literal=lambda v: 'DeserializedValue' if v.value == 'SerializedValue' else None +) + +TestInputObject = GraphQLInputObjectType('TestInputObject', OrderedDict([ + ('a', GraphQLInputObjectField(GraphQLString)), + ('b', GraphQLInputObjectField(GraphQLList(GraphQLString))), + ('c', GraphQLInputObjectField(GraphQLNonNull(GraphQLString))), + ('d', GraphQLInputObjectField(TestComplexScalar)) +])) + +stringify = lambda obj: json.dumps(obj, sort_keys=True) + + +def input_to_json(obj, args, context, info): + input = args.get('input') + if input: + return stringify(input) + + +TestNestedInputObject = GraphQLInputObjectType( + name='TestNestedInputObject', + fields={ + 'na': GraphQLInputObjectField(GraphQLNonNull(TestInputObject)), + 'nb': GraphQLInputObjectField(GraphQLNonNull(GraphQLString)) + } +) + +TestType = GraphQLObjectType('TestType', { + 'fieldWithObjectInput': GraphQLField( + GraphQLString, + args={'input': GraphQLArgument(TestInputObject)}, + resolver=input_to_json), + 'fieldWithNullableStringInput': GraphQLField( + GraphQLString, + args={'input': GraphQLArgument(GraphQLString)}, + resolver=input_to_json), + 'fieldWithNonNullableStringInput': GraphQLField( + GraphQLString, + args={'input': GraphQLArgument(GraphQLNonNull(GraphQLString))}, + resolver=input_to_json), + 'fieldWithDefaultArgumentValue': GraphQLField( + GraphQLString, + args={'input': GraphQLArgument(GraphQLString, 'Hello World')}, + resolver=input_to_json), + 'fieldWithNestedInputObject': GraphQLField( + GraphQLString, + args={'input': GraphQLArgument(TestNestedInputObject, 'Hello World')}, + resolver=input_to_json), + 'list': GraphQLField( + GraphQLString, + args={'input': GraphQLArgument(GraphQLList(GraphQLString))}, + resolver=input_to_json), + 'nnList': GraphQLField( + GraphQLString, + args={'input': GraphQLArgument( + GraphQLNonNull(GraphQLList(GraphQLString)) + )}, + resolver=input_to_json), + 'listNN': GraphQLField( + GraphQLString, + args={'input': GraphQLArgument( + GraphQLList(GraphQLNonNull(GraphQLString)) + )}, + resolver=input_to_json), + 'nnListNN': GraphQLField( + GraphQLString, + args={'input': GraphQLArgument( + GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLString))) + )}, + resolver=input_to_json), +}) + +schema = GraphQLSchema(TestType) + + +def check(doc, expected, args=None): + ast = parse(doc) + response = execute(schema, ast, variable_values=args) + + if response.errors: + result = { + 'data': response.data, + 'errors': [format_error(e) for e in response.errors] + } + else: + result = { + 'data': response.data + } + + assert result == expected + + +# Handles objects and nullability + +def test_inline_executes_with_complex_input(): + doc = ''' + { + fieldWithObjectInput(input: {a: "foo", b: ["bar"], c: "baz"}) + } + ''' + check(doc, { + 'data': {"fieldWithObjectInput": stringify({"a": "foo", "b": ["bar"], "c": "baz"})} + }) + + +def test_properly_parses_single_value_to_list(): + doc = ''' + { + fieldWithObjectInput(input: {a: "foo", b: "bar", c: "baz"}) + } + ''' + check(doc, { + 'data': {'fieldWithObjectInput': stringify({"a": "foo", "b": ["bar"], "c": "baz"})} + }) + + +def test_does_not_use_incorrect_value(): + doc = ''' + { + fieldWithObjectInput(input: ["foo", "bar", "baz"]) + } + ''' + check(doc, { + 'data': {'fieldWithObjectInput': None} + }) + + +def test_properly_runs_parse_literal_on_complex_scalar_types(): + doc = ''' + { + fieldWithObjectInput(input: {a: "foo", d: "SerializedValue"}) + } + ''' + check(doc, { + 'data': { + 'fieldWithObjectInput': '{"a": "foo", "d": "DeserializedValue"}', + } + }) + + +# noinspection PyMethodMayBeStatic +class TestUsingVariables: + doc = ''' + query q($input: TestInputObject) { + fieldWithObjectInput(input: $input) + } + ''' + + def test_executes_with_complex_input(self): + params = {'input': {'a': 'foo', 'b': ['bar'], 'c': 'baz'}} + check(self.doc, { + 'data': {'fieldWithObjectInput': stringify({"a": "foo", "b": ["bar"], "c": "baz"})} + }, params) + + def test_uses_default_value_when_not_provided(self): + with_defaults_doc = ''' + query q($input: TestInputObject = {a: "foo", b: ["bar"], c: "baz"}) { + fieldWithObjectInput(input: $input) + } + ''' + + check(with_defaults_doc, { + 'data': {'fieldWithObjectInput': stringify({"a": "foo", "b": ["bar"], "c": "baz"})} + }) + + def test_properly_parses_single_value_to_list(self): + params = {'input': {'a': 'foo', 'b': 'bar', 'c': 'baz'}} + check(self.doc, { + 'data': {'fieldWithObjectInput': stringify({"a": "foo", "b": ["bar"], "c": "baz"})} + }, params) + + def test_executes_with_complex_scalar_input(self): + params = {'input': {'c': 'foo', 'd': 'SerializedValue'}} + check(self.doc, { + 'data': {'fieldWithObjectInput': stringify({"c": "foo", "d": "DeserializedValue"})} + }, params) + + def test_errors_on_null_for_nested_non_null(self): + params = {'input': {'a': 'foo', 'b': 'bar', 'c': None}} + + with raises(GraphQLError) as excinfo: + check(self.doc, {}, params) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 13, 'line': 2}], + 'message': 'Variable "$input" got invalid value {}.\n' + 'In field "c": Expected "String!", found null.'.format(stringify(params['input'])) + } + + def test_errors_on_incorrect_type(self): + params = {'input': 'foo bar'} + + with raises(GraphQLError) as excinfo: + check(self.doc, {}, params) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 13, 'line': 2}], + 'message': 'Variable "$input" got invalid value {}.\n' + 'Expected "TestInputObject", found not an object.'.format(stringify(params['input'])) + } + + def test_errors_on_omission_of_nested_non_null(self): + params = {'input': {'a': 'foo', 'b': 'bar'}} + + with raises(GraphQLError) as excinfo: + check(self.doc, {}, params) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 13, 'line': 2}], + 'message': 'Variable "$input" got invalid value {}.\n' + 'In field "c": Expected "String!", found null.'.format(stringify(params['input'])) + } + + def test_errors_on_deep_nested_errors_and_with_many_errors(self): + nested_doc = ''' + query q($input: TestNestedInputObject) { + fieldWithNestedObjectInput(input: $input) + } + ''' + + params = {'input': {'na': {'a': 'foo'}}} + with raises(GraphQLError) as excinfo: + check(nested_doc, {}, params) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 19, 'line': 2}], + 'message': 'Variable "$input" got invalid value {}.\n' + 'In field "na": In field "c": Expected "String!", found null.\n' + 'In field "nb": Expected "String!", found null.'.format(stringify(params['input'])) + } + + def test_errors_on_addition_of_input_field_of_incorrect_type(self): + params = {'input': {'a': 'foo', 'b': 'bar', 'c': 'baz', 'd': 'dog'}} + + with raises(GraphQLError) as excinfo: + check(self.doc, {}, params) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 13, 'line': 2}], + 'message': 'Variable "$input" got invalid value {}.\n' + 'In field "d": Expected type "ComplexScalar", found "dog".'.format(stringify(params['input'])) + } + + def test_errors_on_addition_of_unknown_input_field(self): + params = {'input': {'a': 'foo', 'b': 'bar', 'c': 'baz', 'extra': 'dog'}} + + with raises(GraphQLError) as excinfo: + check(self.doc, {}, params) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 13, 'line': 2}], + 'message': 'Variable "$input" got invalid value {}.\n' + 'In field "extra": Unknown field.'.format(stringify(params['input'])) + } + + +def test_allows_nullable_inputs_to_be_omitted(): + doc = '{ fieldWithNullableStringInput }' + check(doc, {'data': { + 'fieldWithNullableStringInput': None + }}) + + +def test_allows_nullable_inputs_to_be_omitted_in_a_variable(): + doc = ''' + query SetsNullable($value: String) { + fieldWithNullableStringInput(input: $value) + } + ''' + + check(doc, { + 'data': { + 'fieldWithNullableStringInput': None + } + }) + + +def test_allows_nullable_inputs_to_be_omitted_in_an_unlisted_variable(): + doc = ''' + query SetsNullable { + fieldWithNullableStringInput(input: $value) + } + ''' + + check(doc, { + 'data': { + 'fieldWithNullableStringInput': None + } + }) + + +def test_allows_nullable_inputs_to_be_set_to_null_in_a_variable(): + doc = ''' + query SetsNullable($value: String) { + fieldWithNullableStringInput(input: $value) + } + ''' + check(doc, { + 'data': { + 'fieldWithNullableStringInput': None + } + }, {'value': None}) + + +def test_allows_nullable_inputs_to_be_set_to_a_value_in_a_variable(): + doc = ''' + query SetsNullable($value: String) { + fieldWithNullableStringInput(input: $value) + } + ''' + + check(doc, { + 'data': { + 'fieldWithNullableStringInput': '"a"' + } + }, {'value': 'a'}) + + +def test_allows_nullable_inputs_to_be_set_to_a_value_directly(): + doc = ''' + { + fieldWithNullableStringInput(input: "a") + } + ''' + check(doc, { + 'data': { + 'fieldWithNullableStringInput': '"a"' + } + }) + + +def test_does_not_allow_non_nullable_inputs_to_be_omitted_in_a_variable(): + doc = ''' + query SetsNonNullable($value: String!) { + fieldWithNonNullableStringInput(input: $value) + } + ''' + with raises(GraphQLError) as excinfo: + check(doc, {}) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 27, 'line': 2}], + 'message': 'Variable "$value" of required type "String!" was not provided.' + } + + +def test_does_not_allow_non_nullable_inputs_to_be_set_to_null_in_a_variable(): + doc = ''' + query SetsNonNullable($value: String!) { + fieldWithNonNullableStringInput(input: $value) + } + ''' + + with raises(GraphQLError) as excinfo: + check(doc, {}, {'value': None}) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 27, 'line': 2}], + 'message': 'Variable "$value" of required type "String!" was not provided.' + } + + +def test_allows_non_nullable_inputs_to_be_set_to_a_value_in_a_variable(): + doc = ''' + query SetsNonNullable($value: String!) { + fieldWithNonNullableStringInput(input: $value) + } + ''' + + check(doc, { + 'data': { + 'fieldWithNonNullableStringInput': '"a"' + } + }, {'value': 'a'}) + + +def test_allows_non_nullable_inputs_to_be_set_to_a_value_directly(): + doc = ''' + { + fieldWithNonNullableStringInput(input: "a") + } + ''' + + check(doc, { + 'data': { + 'fieldWithNonNullableStringInput': '"a"' + } + }) + + +def test_passes_along_null_for_non_nullable_inputs_if_explcitly_set_in_the_query(): + doc = ''' + { + fieldWithNonNullableStringInput + } + ''' + + check(doc, { + 'data': { + 'fieldWithNonNullableStringInput': None + } + }) + + +def test_allows_lists_to_be_null(): + doc = ''' + query q($input: [String]) { + list(input: $input) + } + ''' + + check(doc, { + 'data': { + 'list': None + } + }) + + +def test_allows_lists_to_contain_values(): + doc = ''' + query q($input: [String]) { + list(input: $input) + } + ''' + + check(doc, { + 'data': { + 'list': stringify(['A']) + } + }, {'input': ['A']}) + + +def test_allows_lists_to_contain_null(): + doc = ''' + query q($input: [String]) { + list(input: $input) + } + ''' + + check(doc, { + 'data': { + 'list': stringify(['A', None, 'B']) + } + }, {'input': ['A', None, 'B']}) + + +def test_does_not_allow_non_null_lists_to_be_null(): + doc = ''' + query q($input: [String]!) { + nnList(input: $input) + } + ''' + + with raises(GraphQLError) as excinfo: + check(doc, {}, {'input': None}) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 13, 'line': 2}], + 'message': 'Variable "$input" of required type "[String]!" was not provided.' + } + + +def test_allows_non_null_lists_to_contain_values(): + doc = ''' + query q($input: [String]!) { + nnList(input: $input) + } + ''' + + check(doc, { + 'data': { + 'nnList': stringify(['A']) + } + }, {'input': ['A']}) + + +def test_allows_non_null_lists_to_contain_null(): + doc = ''' + query q($input: [String]!) { + nnList(input: $input) + } + ''' + + check(doc, { + 'data': { + 'nnList': stringify(['A', None, 'B']) + } + }, {'input': ['A', None, 'B']}) + + +def test_allows_lists_of_non_nulls_to_be_null(): + doc = ''' + query q($input: [String!]) { + listNN(input: $input) + } + ''' + + check(doc, { + 'data': { + 'listNN': None + } + }, {'input': None}) + + +def test_allows_lists_of_non_nulls_to_contain_values(): + doc = ''' + query q($input: [String!]) { + listNN(input: $input) + } + ''' + + check(doc, { + 'data': { + 'listNN': stringify(['A']) + } + }, {'input': ['A']}) + + +def test_does_not_allow_lists_of_non_nulls_to_contain_null(): + doc = ''' + query q($input: [String!]) { + listNN(input: $input) + } + ''' + + params = {'input': ['A', None, 'B']} + + with raises(GraphQLError) as excinfo: + check(doc, {}, params) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 13, 'line': 2}], + 'message': 'Variable "$input" got invalid value {}.\n' + 'In element #1: Expected "String!", found null.'.format(stringify(params['input'])) + } + + +def test_does_not_allow_non_null_lists_of_non_nulls_to_be_null(): + doc = ''' + query q($input: [String!]!) { + nnListNN(input: $input) + } + ''' + with raises(GraphQLError) as excinfo: + check(doc, {}, {'input': None}) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 13, 'line': 2}], + 'message': 'Variable "$input" of required type "[String!]!" was not provided.' + } + + +def test_allows_non_null_lists_of_non_nulls_to_contain_values(): + doc = ''' + query q($input: [String!]!) { + nnListNN(input: $input) + } + ''' + + check(doc, { + 'data': { + 'nnListNN': stringify(['A']) + } + }, {'input': ['A']}) + + +def test_does_not_allow_non_null_lists_of_non_nulls_to_contain_null(): + doc = ''' + query q($input: [String!]!) { + nnListNN(input: $input) + } + ''' + + params = {'input': ['A', None, 'B']} + + with raises(GraphQLError) as excinfo: + check(doc, {}, params) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 13, 'line': 2}], + 'message': 'Variable "$input" got invalid value {}.\n' + 'In element #1: Expected "String!", found null.'.format(stringify(params['input'])) + } + + +def test_does_not_allow_invalid_types_to_be_used_as_values(): + doc = ''' + query q($input: TestType!) { + fieldWithObjectInput(input: $input) + } + ''' + params = {'input': {'list': ['A', 'B']}} + + with raises(GraphQLError) as excinfo: + check(doc, {}, params) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 13, 'line': 2}], + 'message': 'Variable "$input" expected value of type "TestType!" which cannot be used as an input type.' + } + + +def test_does_not_allow_unknown_types_to_be_used_as_values(): + doc = ''' + query q($input: UnknownType!) { + fieldWithObjectInput(input: $input) + } + ''' + params = {'input': 'whoknows'} + + with raises(GraphQLError) as excinfo: + check(doc, {}, params) + + assert format_error(excinfo.value) == { + 'locations': [{'column': 13, 'line': 2}], + 'message': 'Variable "$input" expected value of type "UnknownType!" which cannot be used as an input type.' + } + + +# noinspection PyMethodMayBeStatic +class TestUsesArgumentDefaultValues: + + def test_when_no_argument_provided(self): + check('{ fieldWithDefaultArgumentValue }', { + 'data': { + 'fieldWithDefaultArgumentValue': '"Hello World"' + } + }) + + def test_when_nullable_variable_provided(self): + check(''' + query optionalVariable($optional: String) { + fieldWithDefaultArgumentValue(input: $optional) + } + ''', { + 'data': { + 'fieldWithDefaultArgumentValue': '"Hello World"' + } + }) + + def test_when_argument_provided_cannot_be_parsed(self): + check(''' + { + fieldWithDefaultArgumentValue(input: WRONG_TYPE) + } + ''', { + 'data': { + 'fieldWithDefaultArgumentValue': '"Hello World"' + } + }) diff --git a/graphql/execution/experimental/tests/utils.py b/graphql/execution/experimental/tests/utils.py new file mode 100644 index 00000000..89246de1 --- /dev/null +++ b/graphql/execution/experimental/tests/utils.py @@ -0,0 +1,46 @@ +from promise import Promise + +from graphql.execution import ExecutionResult +from graphql.language.parser import parse +from graphql.language.source import Source +from graphql.validation import validate + +from ..executor import execute + + +def resolved(value): + return Promise.fulfilled(value) + + +def rejected(error): + return Promise.rejected(error) + + +def graphql(schema, request_string='', root_value=None, context_value=None, + variable_values=None, operation_name=None, executor=None, + return_promise=False, middleware=None): + try: + source = Source(request_string, 'GraphQL request') + ast = parse(source) + validation_errors = validate(schema, ast) + if validation_errors: + return ExecutionResult( + errors=validation_errors, + invalid=True, + ) + return execute( + schema, + ast, + root_value, + context_value, + operation_name=operation_name, + variable_values=variable_values or {}, + executor=executor, + return_promise=return_promise, + middleware=middleware, + ) + except Exception as e: + return ExecutionResult( + errors=[e], + invalid=True, + ) diff --git a/graphql/execution/experimental/utils.py b/graphql/execution/experimental/utils.py new file mode 100644 index 00000000..8aef421d --- /dev/null +++ b/graphql/execution/experimental/utils.py @@ -0,0 +1,7 @@ +try: + from itertools import imap + normal_map = map +except: + def normal_map(func, iter): + return list(map(func, iter)) + imap = map diff --git a/graphql/execution/tests/test_benchmark.py b/graphql/execution/tests/test_benchmark.py index 7e11d89f..7637eac6 100644 --- a/graphql/execution/tests/test_benchmark.py +++ b/graphql/execution/tests/test_benchmark.py @@ -3,12 +3,15 @@ from graphql import (GraphQLField, GraphQLInt, GraphQLList, GraphQLObjectType, GraphQLSchema, Source, execute, parse) +# from graphql.execution import executor +# executor.use_experimental_executor = True +SIZE = 10000 # set global fixtures Container = namedtuple('Container', 'x y z o') -big_int_list = [x for x in range(5000)] -big_container_list = [Container(x=x, y=x, z=x, o=x) for x in range(5000)] +big_int_list = [x for x in range(SIZE)] +big_container_list = [Container(x=x, y=x, z=x, o=x) for x in range(SIZE)] ContainerType = GraphQLObjectType('Container', fields={ @@ -27,115 +30,47 @@ def resolve_all_ints(root, args, context, info): return big_int_list -def test_big_list_of_ints_to_graphql_schema(benchmark): - @benchmark - def schema(): - Query = GraphQLObjectType('Query', fields={ - 'allInts': GraphQLField( - GraphQLList(GraphQLInt), - resolver=resolve_all_ints - ) - }) - return GraphQLSchema(Query) - - -def test_big_list_of_ints_to_graphql_ast(benchmark): - @benchmark - def ast(): - source = Source('{ allInts }') - return parse(source) - - -def test_big_list_of_ints_to_graphql_partial(benchmark): +def test_big_list_of_ints(benchmark): Query = GraphQLObjectType('Query', fields={ 'allInts': GraphQLField( GraphQLList(GraphQLInt), resolver=resolve_all_ints ) }) - hello_schema = GraphQLSchema(Query) + schema = GraphQLSchema(Query) source = Source('{ allInts }') ast = parse(source) @benchmark def b(): - return partial(execute, hello_schema, ast) - -def test_big_list_of_ints_to_graphql_total(benchmark): - @benchmark - def total(): - Query = GraphQLObjectType('Query', fields={ - 'allInts': GraphQLField( - GraphQLList(GraphQLInt), - resolver=resolve_all_ints - ) - }) - hello_schema = GraphQLSchema(Query) - source = Source('{ allInts }') - ast = parse(source) - return partial(execute, hello_schema, ast) + return execute(schema, ast) -def test_big_list_of_ints_base_serialize(benchmark): +def test_big_list_of_ints_serialize(benchmark): from ..executor import complete_leaf_value @benchmark def serialize(): - for i in big_int_list: - GraphQLInt.serialize(i) + map(GraphQLInt.serialize, big_int_list) -def test_total_big_list_of_containers_with_one_field_schema(benchmark): - @benchmark - def schema(): - Query = GraphQLObjectType('Query', fields={ - 'allContainers': GraphQLField( - GraphQLList(ContainerType), - resolver=resolve_all_containers - ) - }) - return GraphQLSchema(Query) - - -def test_total_big_list_of_containers_with_one_field_parse(benchmark): - @benchmark - def ast(): - source = Source('{ allContainers { x } }') - ast = parse(source) - - -def test_total_big_list_of_containers_with_one_field_partial(benchmark): +def test_big_list_objecttypes_with_one_int_field(benchmark): Query = GraphQLObjectType('Query', fields={ 'allContainers': GraphQLField( GraphQLList(ContainerType), resolver=resolve_all_containers ) }) - hello_schema = GraphQLSchema(Query) + schema = GraphQLSchema(Query) source = Source('{ allContainers { x } }') ast = parse(source) @benchmark def b(): - return partial(execute, hello_schema, ast) + return execute(schema, ast) -def test_total_big_list_of_containers_with_one_field_total(benchmark): - @benchmark - def total(): - Query = GraphQLObjectType('Query', fields={ - 'allContainers': GraphQLField( - GraphQLList(ContainerType), - resolver=resolve_all_containers - ) - }) - hello_schema = GraphQLSchema(Query) - source = Source('{ allContainers { x } }') - ast = parse(source) - result = partial(execute, hello_schema, ast) - - -def test_total_big_list_of_containers_with_multiple_fields_partial(benchmark): +def test_big_list_objecttypes_with_two_int_fields(benchmark): Query = GraphQLObjectType('Query', fields={ 'allContainers': GraphQLField( GraphQLList(ContainerType), @@ -143,26 +78,10 @@ def test_total_big_list_of_containers_with_multiple_fields_partial(benchmark): ) }) - hello_schema = GraphQLSchema(Query) - source = Source('{ allContainers { x, y, z } }') + schema = GraphQLSchema(Query) + source = Source('{ allContainers { x, y } }') ast = parse(source) @benchmark def b(): - return partial(execute, hello_schema, ast) - - -def test_total_big_list_of_containers_with_multiple_fields(benchmark): - @benchmark - def total(): - Query = GraphQLObjectType('Query', fields={ - 'allContainers': GraphQLField( - GraphQLList(ContainerType), - resolver=resolve_all_containers - ) - }) - - hello_schema = GraphQLSchema(Query) - source = Source('{ allContainers { x, y, z } }') - ast = parse(source) - result = partial(execute, hello_schema, ast) + return execute(schema, ast) diff --git a/graphql/execution/tests/test_dataloader.py b/graphql/execution/tests/test_dataloader.py new file mode 100644 index 00000000..3e54a4a6 --- /dev/null +++ b/graphql/execution/tests/test_dataloader.py @@ -0,0 +1,142 @@ +from promise import Promise +from promise.dataloader import DataLoader + +from graphql import GraphQLObjectType, GraphQLField, GraphQLID, GraphQLArgument, GraphQLNonNull, GraphQLSchema, parse, execute + + +def test_batches_correctly(): + + Business = GraphQLObjectType('Business', lambda: { + 'id': GraphQLField(GraphQLID, resolver=lambda root, args, context, info: root), + }) + + Query = GraphQLObjectType('Query', lambda: { + 'getBusiness': GraphQLField(Business, + args={ + 'id': GraphQLArgument(GraphQLNonNull(GraphQLID)), + }, + resolver=lambda root, args, context, info: context.business_data_loader.load(args.get('id')) + ), + }) + + schema = GraphQLSchema(query=Query) + + + doc = ''' +{ + business1: getBusiness(id: "1") { + id + } + business2: getBusiness(id: "2") { + id + } +} + ''' + doc_ast = parse(doc) + + + load_calls = [] + + class BusinessDataLoader(DataLoader): + def batch_load_fn(self, keys): + load_calls.append(keys) + return Promise.resolve(keys) + + class Context(object): + business_data_loader = BusinessDataLoader() + + + result = execute(schema, doc_ast, None, context_value=Context()) + assert not result.errors + assert result.data == { + 'business1': { + 'id': '1' + }, + 'business2': { + 'id': '2' + }, + } + assert load_calls == [['1','2']] + + +def test_batches_multiple_together(): + + Location = GraphQLObjectType('Location', lambda: { + 'id': GraphQLField(GraphQLID, resolver=lambda root, args, context, info: root), + }) + + Business = GraphQLObjectType('Business', lambda: { + 'id': GraphQLField(GraphQLID, resolver=lambda root, args, context, info: root), + 'location': GraphQLField(Location, + resolver=lambda root, args, context, info: context.location_data_loader.load('location-{}'.format(root)) + ), + }) + + Query = GraphQLObjectType('Query', lambda: { + 'getBusiness': GraphQLField(Business, + args={ + 'id': GraphQLArgument(GraphQLNonNull(GraphQLID)), + }, + resolver=lambda root, args, context, info: context.business_data_loader.load(args.get('id')) + ), + }) + + schema = GraphQLSchema(query=Query) + + + doc = ''' +{ + business1: getBusiness(id: "1") { + id + location { + id + } + } + business2: getBusiness(id: "2") { + id + location { + id + } + } +} + ''' + doc_ast = parse(doc) + + + business_load_calls = [] + + class BusinessDataLoader(DataLoader): + def batch_load_fn(self, keys): + business_load_calls.append(keys) + return Promise.resolve(keys) + + location_load_calls = [] + + class LocationDataLoader(DataLoader): + def batch_load_fn(self, keys): + location_load_calls.append(keys) + return Promise.resolve(keys) + + class Context(object): + business_data_loader = BusinessDataLoader() + location_data_loader = LocationDataLoader() + + + result = execute(schema, doc_ast, None, context_value=Context()) + assert not result.errors + assert result.data == { + 'business1': { + 'id': '1', + 'location': { + 'id': 'location-1' + } + }, + 'business2': { + 'id': '2', + 'location': { + 'id': 'location-2' + } + }, + } + assert business_load_calls == [['1','2']] + assert location_load_calls == [['location-1','location-2']] diff --git a/graphql/execution/tests/test_nonnull.py b/graphql/execution/tests/test_nonnull.py index bc48de3f..65d7f098 100644 --- a/graphql/execution/tests/test_nonnull.py +++ b/graphql/execution/tests/test_nonnull.py @@ -81,6 +81,11 @@ def nonNullPromiseNest(self): schema = GraphQLSchema(DataType) +def order_errors(error): + locations = error['locations'] + return (locations[0]['column'], locations[0]['line']) + + def check(doc, data, expected): ast = parse(doc) response = execute(schema, ast, data) @@ -90,12 +95,20 @@ def check(doc, data, expected): 'data': response.data, 'errors': [format_error(e) for e in response.errors] } + if result['errors'] != expected['errors']: + assert result['data'] == expected['data'] + # Sometimes the fields resolves asynchronously, so + # we need to check that the errors are the same, but might be + # raised in a different order. + assert sorted(result['errors'], key=order_errors) == sorted(expected['errors'], key=order_errors) + else: + assert result == expected else: result = { 'data': response.data } - assert result == expected + assert result == expected def test_nulls_a_nullable_field_that_throws_sync(): diff --git a/graphql/pyutils/contain_subset.py b/graphql/pyutils/contain_subset.py index ae8e7535..6c34936d 100644 --- a/graphql/pyutils/contain_subset.py +++ b/graphql/pyutils/contain_subset.py @@ -4,7 +4,10 @@ def contain_subset(expected, actual): t_actual = type(actual) t_expected = type(expected) - if not(issubclass(t_actual, t_expected) or issubclass(t_expected, t_actual)): + actual_is_dict = issubclass(t_actual, dict) + expected_is_dict = issubclass(t_expected, dict) + both_dicts = actual_is_dict and expected_is_dict + if not(both_dicts) and not(issubclass(t_actual, t_expected) or issubclass(t_expected, t_actual)): return False if not isinstance(expected, obj) or expected is None: return expected == actual diff --git a/setup.py b/setup.py index 43120fe4..2a6477f8 100644 --- a/setup.py +++ b/setup.py @@ -20,13 +20,14 @@ install_requires = [ 'six>=1.10.0', - 'promise>=0.4.2' + 'promise>=2.0.dev' ] tests_requires = [ 'pytest==3.0.2', 'pytest-django==2.9.1', 'pytest-cov==2.3.1', + 'coveralls', 'gevent==1.1rc1', 'six>=1.10.0', 'pytest-benchmark==3.0.0',