diff --git a/.travis.yml b/.travis.yml index 5e2f3ebb..5ece4839 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,12 +8,12 @@ python: - pypy cache: pip install: -- pip install --cache-dir $HOME/.cache/pip pytest-cov coveralls flake8 import-order gevent==1.1b5 six>=1.10.0 +- pip install --cache-dir $HOME/.cache/pip pytest-cov pytest-mock coveralls flake8 isort==3.9.6 gevent==1.1b5 six>=1.10.0 pypromise>=0.4.0 - pip install --cache-dir $HOME/.cache/pip pytest>=2.7.3 --upgrade - pip install -e . script: - flake8 -- py.test --cov=graphql tests +- py.test --cov=graphql graphql tests after_success: - coveralls matrix: @@ -21,5 +21,5 @@ matrix: - python: "3.5" script: - flake8 - - import-order graphql - - py.test --cov=graphql tests tests_py35 + - isort --check-only graphql/ -rc + - py.test --cov=graphql graphql tests tests_py35 diff --git a/README.md b/README.md index 06d5b8aa..2f51133f 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,19 @@ # GraphQL-core -GraphQL for Python +GraphQL for Python. + +*This library is a port of [graphql-js](https://github.com/graphql/graphql-js) to Python.* + [![PyPI version](https://badge.fury.io/py/graphql-core.svg)](https://badge.fury.io/py/graphql-core) [![Build Status](https://travis-ci.org/graphql-python/graphql-core.svg?branch=master)](https://travis-ci.org/graphql-python/graphql-core) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphql-core/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphql-core?branch=master) [![Public Slack Discussion](https://graphql-slack.herokuapp.com/badge.svg)](https://graphql-slack.herokuapp.com/) +See more complete documentation at http://graphql.org/ and +http://graphql.org/docs/api-reference-graphql/. -## Project Status - -This library is a port of [graphql-js](https://github.com/graphql/graphql-js) to Python. - -We are currently targeting feature parity with `v0.4.18` of the reference implementation, and are currently on `v0.5.0`. - -Please see [issues](https://github.com/graphql-python/graphql-core/issues) for the progress. +For questions, ask [Stack Overflow](http://stackoverflow.com/questions/tagged/graphql). ## Getting Started @@ -25,7 +24,7 @@ The overview describes a simple set of GraphQL examples that exist as [tests](te in this repository. A good way to get started is to walk through that README and the corresponding tests in parallel. -### Using `graphql-core` +### Using graphql-core Install from pip: @@ -33,23 +32,91 @@ Install from pip: pip install graphql-core ``` -### Supported Python Versions -`graphql-core` supports the following Python versions: - -* `2.7.x` -* `3.3.x` -* `3.4.x` -* `3.5.0` -* `pypy-2.6.1` +GraphQL.js provides two important capabilities: building a type schema, and +serving queries against that type schema. + +First, build a GraphQL type schema which maps to your code base. + +```python +from graphql import ( + graphql, + GraphQLSchema, + GraphQLObjectType, + GraphQLField, + GraphQLString +) + +schema = GraphQLSchema( + query= GraphQLObjectType( + name='RootQueryType', + fields={ + 'hello': GraphQLField( + type= GraphQLString, + resolve=lambda *_: 'world' + ) + } + ) +) +``` + +This defines a simple schema with one type and one field, that resolves +to a fixed value. The `resolve` function can return a value, a promise, +or an array of promises. A more complex example is included in the top +level [tests](graphql/tests) directory. + +Then, serve the result of a query against that type schema. + +```python +query = '{ hello }' + +result = graphql(schema, query) + +# Prints +# { +# "data": { "hello": "world" } +# } +print result +``` + +This runs a query fetching the one field defined. The `graphql` function will +first ensure the query is syntactically and semantically valid before executing +it, reporting errors otherwise. + +```python +query = '{ boyhowdy }' + +result = graphql(schema, query) + +# Prints +# { +# "errors": [ +# { "message": "Cannot query field boyhowdy on RootQueryType", +# "locations": [ { "line": 1, "column": 3 } ] } +# ] +# } +print result +``` -### Built-in Concurrency Support -Support for `3.5.0`'s `asyncio` module for concurrent execution is available via an executor middleware at -`graphql.core.execution.middlewares.asyncio.AsyncioExecutionMiddleware`. +### Executors -Additionally, support for `gevent` is available via -`graphql.core.execution.middlewares.gevent.GeventExecutionMiddleware`. +The graphql query is executed, by default, synchronously (using `SyncExecutor`). +However the following executors are available if we want to resolve our fields in parallel: -Otherwise, by default, the executor will use execute with no concurrency. +* `graphql.execution.executors.asyncio.AsyncioExecutor`: This executor executes the resolvers in the Python asyncio event loop. +* `graphql.execution.executors.asyncio.GeventExecutor`: This executor executes the resolvers in the Gevent event loop. +* `graphql.execution.executors.asyncio.ProcessExecutor`: This executor executes each resolver as a process. +* `graphql.execution.executors.asyncio.ThreadExecutor`: This executor executes each resolver in a Thread. +* `graphql.execution.executors.asyncio.SyncExecutor`: This executor executes each resolver synchronusly (default). + +#### Usage + +You can specify the executor to use via the executor keyword argument in the `grapqhl.execution.execute` function. + +```python +from graphql.execution.execute import execute + +execute(schema, ast, executor=SyncExecutor()) +``` ## Main Contributors diff --git a/graphql/__init__.py b/graphql/__init__.py index 7af7fc2e..65caaf12 100644 --- a/graphql/__init__.py +++ b/graphql/__init__.py @@ -1,5 +1,5 @@ ''' -GraphQL provides a Python implementation for the GraphQL specification +GraphQL.js provides a reference implementation for the GraphQL specification but is also a useful utility for operating on GraphQL files and building sophisticated tools. @@ -14,4 +14,200 @@ This also includes utility functions for operating on GraphQL types and GraphQL documents to facilitate building tools. + +You may also import from each sub-directory directly. For example, the +following two import statements are equivalent: + + from graphql import parse + from graphql.language.base import parse ''' + + +# The primary entry point into fulfilling a GraphQL request. +from .graphql import ( + graphql +) + + +# Create and operate on GraphQL type definitions and schema. +from .type import ( # no import order + GraphQLSchema, + + # Definitions + GraphQLScalarType, + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLUnionType, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLList, + GraphQLNonNull, + + # Scalars + GraphQLInt, + GraphQLFloat, + GraphQLString, + GraphQLBoolean, + GraphQLID, + + # Predicates + is_type, + is_input_type, + is_output_type, + is_leaf_type, + is_composite_type, + is_abstract_type, + + # Un-modifiers + get_nullable_type, + get_named_type, +) + + +# Parse and operate on GraphQL language source files. +from .language.base import ( # no import order + Source, + get_location, + + # Parse + parse, + parse_value, + + # Print + print_ast, + + # Visit + visit, + ParallelVisitor, + TypeInfoVisitor, + BREAK, +) + + +# Execute GraphQL queries. +from .execution import ( # no import order + execute, +) + + +# Validate GraphQL queries. +from .validation import ( # no import order + validate, + specified_rules, +) + +# Create and format GraphQL errors. +from .error import ( + GraphQLError, + format_error, +) + + +# Utilities for operating on GraphQL type schema and parsed sources. +from .utils.base import ( + # The GraphQL query recommended for a full schema introspection. + introspection_query, + + # Gets the target Operation from a Document + get_operation_ast, + + # Build a GraphQLSchema from an introspection result. + build_client_schema, + + # Build a GraphQLSchema from a parsed GraphQL Schema language AST. + build_ast_schema, + + # Extends an existing GraphQLSchema from a parsed GraphQL Schema + # language AST. + extend_schema, + + # Print a GraphQLSchema to GraphQL Schema language. + print_schema, + + # Create a GraphQLType from a GraphQL language AST. + type_from_ast, + + # Create a JavaScript value from a GraphQL language AST. + value_from_ast, + + # Create a GraphQL language AST from a JavaScript value. + ast_from_value, + + # A helper to use within recursive-descent visitors which need to be aware of + # the GraphQL type system. + TypeInfo, + + # Determine if JavaScript values adhere to a GraphQL type. + is_valid_value, + + # Determine if AST values adhere to a GraphQL type. + is_valid_literal_value, + + # Concatenates multiple AST together. + concat_ast, + + # Comparators for types + is_equal_type, + is_type_sub_type_of, + do_types_overlap, + + # Asserts a string is a valid GraphQL name. + assert_valid_name, +) + +__all__ = ( + 'graphql', + 'GraphQLBoolean', + 'GraphQLEnumType', + 'GraphQLFloat', + 'GraphQLID', + 'GraphQLInputObjectType', + 'GraphQLInt', + 'GraphQLInterfaceType', + 'GraphQLList', + 'GraphQLNonNull', + 'GraphQLObjectType', + 'GraphQLScalarType', + 'GraphQLSchema', + 'GraphQLString', + 'GraphQLUnionType', + 'get_named_type', + 'get_nullable_type', + 'is_abstract_type', + 'is_composite_type', + 'is_input_type', + 'is_leaf_type', + 'is_output_type', + 'is_type', + 'BREAK', + 'ParallelVisitor', + 'Source', + 'TypeInfoVisitor', + 'get_location', + 'parse', + 'parse_value', + 'print_ast', + 'visit', + 'execute', + 'specified_rules', + 'validate', + 'GraphQLError', + 'format_error', + 'TypeInfo', + 'assert_valid_name', + 'ast_from_value', + 'build_ast_schema', + 'build_client_schema', + 'concat_ast', + 'do_types_overlap', + 'extend_schema', + 'get_operation_ast', + 'introspection_query', + 'is_equal_type', + 'is_type_sub_type_of', + 'is_valid_literal_value', + 'is_valid_value', + 'print_schema', + 'type_from_ast', + 'value_from_ast', +) diff --git a/graphql/core/__init__.py b/graphql/core/__init__.py deleted file mode 100644 index 0cec405d..00000000 --- a/graphql/core/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -from .execution import ExecutionResult, execute -from .language.parser import parse -from .language.source import Source -from .validation import validate - - -def graphql(schema, request='', root=None, args=None, operation_name=None): - try: - source = Source(request, 'GraphQL request') - ast = parse(source) - validation_errors = validate(schema, ast) - if validation_errors: - return ExecutionResult( - errors=validation_errors, - invalid=True, - ) - return execute( - schema, - root or object(), - ast, - operation_name, - args or {}, - ) - except Exception as e: - return ExecutionResult( - errors=[e], - invalid=True, - ) diff --git a/graphql/core/execution/__init__.py b/graphql/core/execution/__init__.py deleted file mode 100644 index 2f7f2830..00000000 --- a/graphql/core/execution/__init__.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Terminology - -"Definitions" are the generic name for top-level statements in the document. -Examples of this include: -1) Operations (such as a query) -2) Fragments - -"Operations" are a generic name for requests in the document. -Examples of this include: -1) query, -2) mutation - -"Selections" are the statements that can appear legally and at -single level of the query. These include: -1) field references e.g "a" -2) fragment "spreads" e.g. "...c" -3) inline fragment "spreads" e.g. "...on Type { a }" -""" - -from .base import ExecutionResult -from .executor import Executor -from .middlewares.sync import SynchronousExecutionMiddleware - - -def execute(schema, root, ast, operation_name='', args=None): - """ - Executes an AST synchronously. Assumes that the AST is already validated. - """ - return get_default_executor().execute(schema, ast, root, args, operation_name, validate_ast=False) - - -_default_executor = None - - -def get_default_executor(): - """ - Gets the default executor to be used in the `execute` function above. - """ - global _default_executor - if _default_executor is None: - _default_executor = Executor([SynchronousExecutionMiddleware()]) - - return _default_executor - - -def set_default_executor(executor): - """ - Sets the default executor to be used in the `execute` function above. - - If passed `None` will reset to the original default synchronous executor. - """ - assert isinstance(executor, Executor) or executor is None - global _default_executor - _default_executor = executor - - -__all__ = ['ExecutionResult', 'Executor', 'execute', 'get_default_executor', 'set_default_executor'] diff --git a/graphql/core/execution/executor.py b/graphql/core/execution/executor.py deleted file mode 100644 index 92aec6b3..00000000 --- a/graphql/core/execution/executor.py +++ /dev/null @@ -1,325 +0,0 @@ -import collections -import functools - -from ..error import GraphQLError -from ..language import ast -from ..language.parser import parse -from ..language.source import Source -from ..pyutils.default_ordered_dict import DefaultOrderedDict -from ..pyutils.defer import (Deferred, DeferredDict, DeferredList, defer, - succeed) -from ..type import (GraphQLEnumType, GraphQLInterfaceType, GraphQLList, - GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, - GraphQLUnionType) -from ..validation import validate -from .base import (ExecutionContext, ExecutionResult, ResolveInfo, Undefined, - collect_fields, default_resolve_fn, get_field_def, - get_operation_root_type) - - -class Executor(object): - - def __init__(self, execution_middlewares=None, default_resolver=default_resolve_fn, map_type=dict): - assert issubclass(map_type, collections.MutableMapping) - - self._execution_middlewares = execution_middlewares or [] - self._default_resolve_fn = default_resolver - self._map_type = map_type - self._enforce_strict_ordering = issubclass(map_type, collections.OrderedDict) - - @property - def enforce_strict_ordering(self): - return self._enforce_strict_ordering - - @property - def map_type(self): - return self._map_type - - def execute(self, schema, request='', root=None, args=None, operation_name=None, request_context=None, - execute_serially=False, validate_ast=True): - - curried_execution_function = functools.partial( - self._execute, - schema, - request, - root, - args, - operation_name, - request_context, - execute_serially, - validate_ast - ) - - for middleware in self._execution_middlewares: - if hasattr(middleware, 'execution_result'): - curried_execution_function = functools.partial(middleware.execution_result, curried_execution_function) - - return curried_execution_function() - - def _execute(self, schema, request, root, args, operation_name, request_context, execute_serially, validate_ast): - if not isinstance(request, ast.Document): - if not isinstance(request, Source): - request = Source(request, 'GraphQL request') - - request = parse(request) - - if validate_ast: - validation_errors = validate(schema, request) - if validation_errors: - return succeed(ExecutionResult( - errors=validation_errors, - invalid=True, - )) - - return self._execute_graphql_query( - schema, - root or object(), - request, - operation_name, - args or {}, - request_context or {}, - execute_serially) - - def _execute_graphql_query(self, schema, root, ast, operation_name, args, request_context, execute_serially=False): - ctx = ExecutionContext(schema, root, ast, operation_name, args, request_context) - - return defer(self._execute_operation, ctx, root, ctx.operation, execute_serially) \ - .add_errback( - lambda error: ctx.errors.append(error) - ) \ - .add_callback( - lambda data: ExecutionResult(data, ctx.errors), - ) - - def _execute_operation(self, ctx, root, operation, execute_serially): - type = get_operation_root_type(ctx.schema, operation) - - if operation.operation == 'mutation' or execute_serially: - execute_serially = True - - fields = DefaultOrderedDict(list) \ - if (execute_serially or self._enforce_strict_ordering) \ - else collections.defaultdict(list) - - fields = collect_fields(ctx, type, operation.selection_set, fields, set()) - - if execute_serially: - return self._execute_fields_serially(ctx, type, root, fields) - - return self._execute_fields(ctx, type, root, fields) - - def _execute_fields_serially(self, execution_context, parent_type, source_value, fields): - def execute_field_callback(results, response_name): - field_asts = fields[response_name] - result = self._resolve_field(execution_context, parent_type, source_value, field_asts) - if result is Undefined: - return results - - def collect_result(resolved_result): - results[response_name] = resolved_result - return results - - if isinstance(result, Deferred): - return succeed(result).add_callback(collect_result) - - else: - return collect_result(result) - - def execute_field(prev_deferred, response_name): - return prev_deferred.add_callback(execute_field_callback, response_name) - - return functools.reduce(execute_field, fields.keys(), succeed(self._map_type())) - - def _execute_fields(self, execution_context, parent_type, source_value, fields): - contains_deferred = False - - results = self._map_type() - for response_name, field_asts in fields.items(): - result = self._resolve_field(execution_context, parent_type, source_value, field_asts) - if result is Undefined: - continue - - results[response_name] = result - if isinstance(result, Deferred): - contains_deferred = True - - if not contains_deferred: - return results - - return DeferredDict(results) - - def _resolve_field(self, execution_context, parent_type, source, field_asts): - field_ast = field_asts[0] - field_name = field_ast.name.value - - field_def = get_field_def(execution_context.schema, parent_type, field_name) - if not field_def: - return Undefined - - return_type = field_def.type - resolve_fn = field_def.resolver or self._default_resolve_fn - - # Build a dict of arguments from the field.arguments AST, using the variables scope to - # fulfill any variable references. - args = execution_context.get_argument_values(field_def, field_ast) - - # The resolve function's optional third argument is a collection of - # information about the current execution state. - info = ResolveInfo( - field_name, - field_asts, - return_type, - parent_type, - execution_context - ) - - result = self.resolve_or_error(resolve_fn, source, args, info) - return self.complete_value_catching_error( - execution_context, return_type, field_asts, info, result - ) - - def complete_value_catching_error(self, ctx, return_type, field_asts, info, result): - # If the field type is non-nullable, then it is resolved without any - # protection from errors. - if isinstance(return_type, GraphQLNonNull): - return self.complete_value(ctx, return_type, field_asts, info, result) - - # Otherwise, error protection is applied, logging the error and - # resolving a null value for this field if one is encountered. - try: - completed = self.complete_value(ctx, return_type, field_asts, info, result) - if isinstance(completed, Deferred): - def handle_error(error): - ctx.errors.append(error) - return None - - return completed.add_errback(handle_error) - - return completed - except Exception as e: - ctx.errors.append(e) - return None - - def complete_value(self, ctx, return_type, field_asts, info, result): - """ - Implements the instructions for completeValue as defined in the - "Field entries" section of the spec. - - If the field type is Non-Null, then this recursively completes the value for the inner type. It throws a field - error if that completion returns null, as per the "Nullability" section of the spec. - - If the field type is a List, then this recursively completes the value for the inner type on each item in the - list. - - If the field type is a Scalar or Enum, ensures the completed value is a legal value of the type by calling the - `serialize` method of GraphQL type definition. - - Otherwise, the field type expects a sub-selection set, and will complete the value by evaluating all - sub-selections. - """ - # If field type is NonNull, complete for inner type, and throw field error if result is null. - if isinstance(result, Deferred): - return result.add_callbacks( - lambda resolved: self.complete_value( - ctx, - return_type, - field_asts, - info, - resolved - ), - lambda error: GraphQLError(error.value and str(error.value), field_asts, error) - ) - - if isinstance(result, Exception): - raise GraphQLError(str(result), field_asts, result) - - if isinstance(return_type, GraphQLNonNull): - completed = self.complete_value( - ctx, return_type.of_type, field_asts, info, result - ) - if completed is None: - raise GraphQLError( - 'Cannot return null for non-nullable field {}.{}.'.format(info.parent_type, info.field_name), - field_asts - ) - - return completed - - # If result is null-like, return null. - if result is None: - return None - - # If field type is List, complete each item in the list with the inner type - if isinstance(return_type, GraphQLList): - assert isinstance(result, collections.Iterable), \ - ('User Error: expected iterable, but did not find one' + - 'for field {}.{}').format(info.parent_type, info.field_name) - - item_type = return_type.of_type - completed_results = [] - contains_deferred = False - for item in result: - completed_item = self.complete_value_catching_error(ctx, item_type, field_asts, info, item) - if not contains_deferred and isinstance(completed_item, Deferred): - contains_deferred = True - - completed_results.append(completed_item) - - return DeferredList(completed_results) if contains_deferred else completed_results - - # If field type is Scalar or Enum, serialize to a valid value, returning null if coercion is not possible. - if isinstance(return_type, (GraphQLScalarType, GraphQLEnumType)): - serialized_result = return_type.serialize(result) - - if serialized_result is None: - return None - - return serialized_result - - runtime_type = None - - # Field type must be Object, Interface or Union and expect sub-selections. - if isinstance(return_type, GraphQLObjectType): - runtime_type = return_type - - elif isinstance(return_type, (GraphQLInterfaceType, GraphQLUnionType)): - runtime_type = return_type.resolve_type(result, info) - if runtime_type and not return_type.is_possible_type(runtime_type): - raise GraphQLError( - u'Runtime Object type "{}" is not a possible type for "{}".'.format(runtime_type, return_type), - field_asts - ) - - if not runtime_type: - return None - - if runtime_type.is_type_of and not runtime_type.is_type_of(result, info): - raise GraphQLError( - u'Expected value of type "{}" but got {}.'.format(return_type, type(result).__name__), - field_asts - ) - - # Collect sub-fields to execute to complete this value. - subfield_asts = DefaultOrderedDict(list) if self._enforce_strict_ordering else collections.defaultdict(list) - visited_fragment_names = set() - for field_ast in field_asts: - selection_set = field_ast.selection_set - if selection_set: - subfield_asts = collect_fields( - ctx, runtime_type, selection_set, - subfield_asts, visited_fragment_names - ) - - return self._execute_fields(ctx, runtime_type, result, subfield_asts) - - def resolve_or_error(self, resolve_fn, source, args, info): - curried_resolve_fn = functools.partial(resolve_fn, source, args, info) - - try: - for middleware in self._execution_middlewares: - if hasattr(middleware, 'run_resolve_fn'): - curried_resolve_fn = functools.partial(middleware.run_resolve_fn, curried_resolve_fn, resolve_fn) - - return curried_resolve_fn() - except Exception as e: - return e diff --git a/graphql/core/execution/middlewares/__init__.py b/graphql/core/execution/middlewares/__init__.py deleted file mode 100644 index 9db7df9a..00000000 --- a/graphql/core/execution/middlewares/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'jake' diff --git a/graphql/core/execution/middlewares/asyncio.py b/graphql/core/execution/middlewares/asyncio.py deleted file mode 100644 index ef95602e..00000000 --- a/graphql/core/execution/middlewares/asyncio.py +++ /dev/null @@ -1,40 +0,0 @@ -# flake8: noqa -from asyncio import Future, ensure_future, iscoroutine - -from ...pyutils.defer import Deferred - - -def process_future_result(deferred): - def handle_future_result(future): - exception = future.exception() - if exception: - deferred.errback(exception) - - else: - deferred.callback(future.result()) - - return handle_future_result - - -class AsyncioExecutionMiddleware(object): - - @staticmethod - def run_resolve_fn(resolver, original_resolver): - result = resolver() - if isinstance(result, Future) or iscoroutine(result): - future = ensure_future(result) - d = Deferred() - future.add_done_callback(process_future_result(d)) - return d - - return result - - @staticmethod - def execution_result(executor): - future = Future() - result = executor() - assert isinstance(result, Deferred), 'Another middleware has converted the execution result ' \ - 'away from a Deferred.' - - result.add_callbacks(future.set_result, future.set_exception) - return future diff --git a/graphql/core/execution/middlewares/gevent.py b/graphql/core/execution/middlewares/gevent.py deleted file mode 100644 index 1b000c02..00000000 --- a/graphql/core/execution/middlewares/gevent.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import absolute_import - -from gevent import get_hub, spawn -from gevent.event import AsyncResult - -from ...pyutils.defer import Deferred, DeferredException -from .utils import resolver_has_tag, tag_resolver - - -def _run_resolver_in_greenlet(d, resolver): - try: - result = resolver() - get_hub().loop.run_callback(d.callback, result) - except: - e = DeferredException() - get_hub().loop.run_callback(d.errback, e) - - -def run_in_greenlet(f): - """ - Marks a resolver to run inside a greenlet. - - @run_in_greenlet - def resolve_something(context, _*): - gevent.sleep(1) - return 5 - - """ - return tag_resolver(f, 'run_in_greenlet') - - -class GeventExecutionMiddleware(object): - - @staticmethod - def run_resolve_fn(resolver, original_resolver): - if resolver_has_tag(original_resolver, 'run_in_greenlet'): - d = Deferred() - spawn(_run_resolver_in_greenlet, d, resolver) - return d - - return resolver() - - @staticmethod - def execution_result(executor): - result = AsyncResult() - deferred = executor() - assert isinstance(deferred, Deferred), 'Another middleware has converted the execution result ' \ - 'away from a Deferred.' - - deferred.add_callbacks(result.set, lambda e: result.set_exception(e.value, (e.type, e.value, e.traceback))) - return result.get() diff --git a/graphql/core/execution/middlewares/sync.py b/graphql/core/execution/middlewares/sync.py deleted file mode 100644 index a0fa8bbc..00000000 --- a/graphql/core/execution/middlewares/sync.py +++ /dev/null @@ -1,18 +0,0 @@ -from ...error import GraphQLError -from ...pyutils.defer import Deferred - - -class SynchronousExecutionMiddleware(object): - - @staticmethod - def run_resolve_fn(resolver, original_resolver): - result = resolver() - if isinstance(result, Deferred): - raise GraphQLError('You cannot return a Deferred from a resolver when using SynchronousExecutionMiddleware') - - return result - - @staticmethod - def execution_result(executor): - result = executor() - return result.result diff --git a/graphql/core/execution/middlewares/utils.py b/graphql/core/execution/middlewares/utils.py deleted file mode 100644 index 1f64db10..00000000 --- a/graphql/core/execution/middlewares/utils.py +++ /dev/null @@ -1,33 +0,0 @@ -def tag_resolver(f, tag): - """ - Tags a resolver function with a specific tag that can be read by a Middleware to denote specific functionality. - :param f: The function to tag. - :param tag: The tag to add to the function. - :return: The function with the tag added. - """ - if not hasattr(f, '_resolver_tags'): - f._resolver_tags = set() - - f._resolver_tags.add(tag) - return f - - -def resolver_has_tag(f, tag): - """ - Checks to see if a function has a specific tag. - """ - if not hasattr(f, '_resolver_tags'): - return False - - return tag in f._resolver_tags - - -def merge_resolver_tags(source_resolver, target_resolver): - if not hasattr(source_resolver, '_resolver_tags'): - return target_resolver - - if not hasattr(target_resolver, '_resolver_tags'): - target_resolver._resolver_tags = set() - - target_resolver._resolver_tags |= source_resolver._resolver_tags - return target_resolver diff --git a/graphql/core/pyutils/defer.py b/graphql/core/pyutils/defer.py deleted file mode 100644 index 0bafe88f..00000000 --- a/graphql/core/pyutils/defer.py +++ /dev/null @@ -1,529 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -"""Small framework for asynchronous programming.""" -# Copyright (C) 2008-2010 Sebastian Heinlein -# Copyright (c) 2001-2010 -# Allen Short -# Andy Gayton -# Andrew Bennetts -# Antoine Pitrou -# Apple Computer, Inc. -# Benjamin Bruheim -# Bob Ippolito -# Canonical Limited -# Christopher Armstrong -# David Reid -# Donovan Preston -# Eric Mangold -# Eyal Lotem -# Itamar Shtull-Trauring -# James Knight -# Jason A. Mobarak -# Jean-Paul Calderone -# Jessica McKellar -# Jonathan Jacobs -# Jonathan Lange -# Jonathan D. Simms -# Jürgen Hermann -# Kevin Horn -# Kevin Turner -# Mary Gardiner -# Matthew Lefkowitz -# Massachusetts Institute of Technology -# Moshe Zadka -# Paul Swartz -# Pavel Pergamenshchik -# Ralph Meijer -# Sean Riley -# Software Freedom Conservancy -# Travis B. Hartwell -# Thijs Triemstra -# Thomas Herve -# Timothy Allen -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -import collections -import sys - -from six import reraise - -__all__ = ("Deferred", "AlreadyCalledDeferred", "DeferredException", - "defer", "succeed", "fail", "DeferredDict", "DeferredList") - - -class AlreadyCalledDeferred(Exception): - """The Deferred is already running a callback.""" - - -class DeferredException(object): - """Allows to defer exceptions.""" - __slots__ = 'type', 'value', 'traceback' - - def __init__(self, type=None, value=None, traceback=None): - """Return a new DeferredException instance. - - If type, value and traceback are not specified the infotmation - will be retreieved from the last caught exception: - - >>> try: - ... raise Exception("Test") - ... except: - ... deferred_exc = DeferredException() - >>> deferred_exc.raise_exception() - Traceback (most recent call last): - ... - Exception: Test - - Alternatively you can set the exception manually: - - >>> exception = Exception("Test 2") - >>> deferred_exc = DeferredException(exception) - >>> deferred_exc.raise_exception() - Traceback (most recent call last): - ... - Exception: Test 2 - """ - self.type = type - self.value = value - self.traceback = traceback - if isinstance(type, Exception): - self.type = type.__class__ - self.value = type - elif not type or not value: - self.type, self.value, self.traceback = sys.exc_info() - - def raise_exception(self): - """Raise the stored exception.""" - reraise(self.type, self.value, self.traceback) - - def catch(self, *errors): - """Check if the stored exception is a subclass of one of the - provided exception classes. If this is the case return the - matching exception class. Otherwise raise the stored exception. - - >>> exc = DeferredException(SystemError()) - >>> exc.catch(Exception) # Will catch the exception and return it - - >>> exc.catch(OSError) # Won't catch and raise the stored exception - Traceback (most recent call last): - ... - SystemError - - This method can be used in errbacks of a Deferred: - - >>> def dummy_errback(deferred_exception): - ... '''Error handler for OSError''' - ... deferred_exception.catch(OSError) - ... return "catched" - - The above errback can handle an OSError: - - >>> deferred = Deferred() - >>> deferred.add_errback(dummy_errback) - >>> deferred.errback(OSError()) - >>> deferred.result - 'catched' - - But fails to handle a SystemError: - - >>> deferred2 = Deferred() - >>> deferred2.add_errback(dummy_errback) - >>> deferred2.errback(SystemError()) - >>> deferred2.result #doctest: +ELLIPSIS - - >>> deferred2.result.value - SystemError() - """ - for err in errors: - if issubclass(self.type, err): - return err - self.raise_exception() - - -class Deferred(object): - """The Deferred allows to chain callbacks. - - There are two type of callbacks: normal callbacks and errbacks, which - handle an exception in a normal callback. - - The callbacks are processed in pairs consisting of a normal callback - and an errback. A normal callback will return its result to the - callback of the next pair. If an exception occurs, it will be handled - by the errback of the next pair. If an errback doesn't raise an error - again, the callback of the next pair will be called with the return - value of the errback. Otherwise the exception of the errback will be - returned to the errback of the next pair:: - - CALLBACK1 ERRBACK1 - | \ / | - result failure result failure - | \ / | - | \ / | - | X | - | / \ | - | / \ | - | / \ | - CALLBACK2 ERRBACK2 - | \ / | - result failure result failure - | \ / | - | \ / | - | X | - | / \ | - | / \ | - | / \ | - CALLBACK3 ERRBACK3 - """ - - __slots__ = 'callbacks', 'errbacks', 'called', 'paused', '_running', 'result' - - def __init__(self): - """Return a new Deferred instance.""" - self.callbacks = [] - self.errbacks = [] - self.called = False - self.paused = False - self._running = False - - def add_callbacks(self, callback, errback=None, - callback_args=None, callback_kwargs=None, - errback_args=None, errback_kwargs=None): - """Add a pair of callables (function or method) to the callback and - errback chain. - - Keyword arguments: - callback -- the next chained challback - errback -- the next chained errback - callback_args -- list of additional arguments for the callback - callback_kwargs -- dict of additional arguments for the callback - errback_args -- list of additional arguments for the errback - errback_kwargs -- dict of additional arguments for the errback - - In the following example the first callback pairs raises an - exception that is catched by the errback of the second one and - processed by the third one. - - >>> def callback(previous): - ... '''Return the previous result.''' - ... return "Got: %s" % previous - >>> def callback_raise(previous): - ... '''Fail and raise an exception.''' - ... raise Exception("Test") - >>> def errback(error): - ... '''Recover from an exception.''' - ... #error.catch(Exception) - ... return "catched" - >>> deferred = Deferred() - >>> deferred.callback("start") - >>> deferred.result - 'start' - >>> deferred.add_callbacks(callback_raise, errback) - >>> deferred.result #doctest: +ELLIPSIS - - >>> deferred.add_callbacks(callback, errback) - >>> deferred.result - 'catched' - >>> deferred.add_callbacks(callback, errback) - >>> deferred.result - 'Got: catched' - """ - assert callback is _passthrough or isinstance(callback, collections.Callable) - assert errback is None or errback is _passthrough or isinstance(errback, collections.Callable) - if errback is None: - errback = _passthrough - self.callbacks.append(((callback, - callback_args or ([]), - callback_kwargs or ({})), - (errback or (_passthrough), - errback_args or ([]), - errback_kwargs or ({})))) - if self.called: - self._next() - - return self - - def add_errback(self, func, *args, **kwargs): - """Add a callable (function or method) to the errback chain only. - - If there isn't any exception the result will be passed through to - the callback of the next pair. - - The first argument is the callable instance followed by any - additional argument that will be passed to the errback. - - The errback method will get the most recent DeferredException and - and any additional arguments that was specified in add_errback. - - If the errback can catch the exception it can return a value that - will be passed to the next callback in the chain. Otherwise the - errback chain will not be processed anymore. - - See the documentation of defer.DeferredException.catch for - further information. - - >>> def catch_error(deferred_error, ignore=False): - ... if ignore: - ... return "ignored" - ... deferred_error.catch(Exception) - ... return "catched" - >>> deferred = Deferred() - >>> deferred.errback(SystemError()) - >>> deferred.add_errback(catch_error, ignore=True) - >>> deferred.result - 'ignored' - """ - return self.add_callbacks(_passthrough, func, errback_args=args, - errback_kwargs=kwargs) - - def add_callback(self, func, *args, **kwargs): - """Add a callable (function or method) to the callback chain only. - - An error would be passed through to the next errback. - - The first argument is the callable instance followed by any - additional argument that will be passed to the callback. - - The callback method will get the result of the previous callback - and any additional arguments that was specified in add_callback. - - >>> def callback(previous, counter=False): - ... if counter: - ... return previous + 1 - ... return previous - >>> deferred = Deferred() - >>> deferred.add_callback(callback, counter=True) - >>> deferred.callback(1) - >>> deferred.result - 2 - """ - return self.add_callbacks(func, _passthrough, callback_args=args, - callback_kwargs=kwargs) - - def errback(self, error=None): - """Start processing the errorback chain starting with the - provided exception or DeferredException. - - If an exception is specified it will be wrapped into a - DeferredException. It will be send to the first errback or stored - as finally result if not any further errback has been specified yet. - - >>> deferred = Deferred() - >>> deferred.errback(Exception("Test Error")) - >>> deferred.result #doctest: +ELLIPSIS - - >>> deferred.result.raise_exception() - Traceback (most recent call last): - ... - Exception: Test Error - """ - if self.called: - raise AlreadyCalledDeferred() - if not error: - error = DeferredException() - elif not isinstance(error, DeferredException): - assert isinstance(error, Exception) - error = DeferredException(error.__class__, error, None) - - self.called = True - self.result = error - self._next() - - def callback(self, result=None): - """Start processing the callback chain starting with the - provided result. - - It will be send to the first callback or stored as finally - one if not any further callback has been specified yet. - - >>> deferred = Deferred() - >>> deferred.callback("done") - >>> deferred.result - 'done' - """ - if self.called: - raise AlreadyCalledDeferred() - self.called = True - - if isinstance(result, Deferred): - self.paused = True - return result.add_callbacks(self._continue, self._continue) - - self.result = result - self._next() - - def _continue(self, result): - """Continue processing the Deferred with the given result.""" - # If the result of the deferred is another deferred, we will need to wait for - # it to resolve again. - if isinstance(result, Deferred): - return result.add_callbacks(self._continue, self._continue) - - self.result = result - self.paused = False - if self.called: - self._next() - - return result - - def _next(self): - """Process the next callback.""" - if self._running or self.paused: - return - - while self.callbacks: - # Get the next callback pair - next_pair = self.callbacks.pop(0) - # Continue with the errback if the last result was an exception - callback, args, kwargs = next_pair[isinstance(self.result, - DeferredException)] - - if callback is not _passthrough: - self._running = True - try: - self.result = callback(self.result, *args, **kwargs) - - except: - self.result = DeferredException() - - finally: - self._running = False - - if isinstance(self.result, Exception): - self.result = DeferredException(self.result) - - if isinstance(self.result, Deferred): - # If a Deferred was returned add this deferred as callbacks to - # the returned one. As a result the processing of this Deferred - # will be paused until all callbacks of the returned Deferred - # have been performed - self.paused = True - self.result.add_callbacks(self._continue, self._continue) - break - - -def defer(func, *args, **kwargs): - """Invoke the given function that may or not may be a Deferred. - - If the return object of the function call is a Deferred return, it. - Otherwise wrap it into a Deferred. - - >>> defer(lambda x: x, 10) #doctest: +ELLIPSIS - - - >>> deferred = defer(lambda x: x, "done") - >>> deferred.result - 'done' - - >>> deferred = Deferred() - >>> defer(lambda: deferred) == deferred - True - """ - assert isinstance(func, collections.Callable) - - try: - result = func(*args, **kwargs) - except: - result = DeferredException() - - if isinstance(result, Deferred): - return result - - deferred = Deferred() - deferred.callback(result) - return deferred - - -_passthrough = object() - - -def succeed(result): - d = Deferred() - d.callback(result) - return d - - -def fail(result=None): - d = Deferred() - d.errback(result) - return d - - -class _ResultCollector(Deferred): - objects_remaining_to_resolve = 0 - _result = None - - def _schedule_callbacks(self, items, result, objects_remaining_to_resolve=None, preserve_insert_ordering=False): - self.objects_remaining_to_resolve = \ - objects_remaining_to_resolve if objects_remaining_to_resolve is not None else len(items) - self._result = result - for key, value in items: - if isinstance(value, Deferred): - # We will place a value in place of the resolved key, so that insert order is preserved. - if preserve_insert_ordering: - result[key] = None - - value.add_callbacks(self._cb_deferred, self._cb_deferred, - callback_args=(key, True), - errback_args=(key, False)) - else: - self.objects_remaining_to_resolve -= 1 - result[key] = value - - if self.objects_remaining_to_resolve == 0 and not self.called: - self.callback(self._result) - self._result = None - - def _cb_deferred(self, result, key, succeeded): - # If one item fails, we are going to errback right away with the error. - # This follows the Promise.all(...) spec in ES6. - if self.called: - return result - - if not succeeded: - self.errback(result) - self._result = None - return result - - self.objects_remaining_to_resolve -= 1 - self._result[key] = result - - if self.objects_remaining_to_resolve == 0: - self.callback(self._result) - self._result = None - - return result - - -class DeferredDict(_ResultCollector): - - def __init__(self, mapping): - super(DeferredDict, self).__init__() - assert isinstance(mapping, collections.Mapping) - self._schedule_callbacks(mapping.items(), type(mapping)(), - preserve_insert_ordering=isinstance(mapping, collections.OrderedDict)) - - -class DeferredList(_ResultCollector): - - def __init__(self, sequence): - super(DeferredList, self).__init__() - assert isinstance(sequence, collections.Sequence) - sequence_len = len(sequence) - self._schedule_callbacks(enumerate(sequence), [None] * sequence_len, sequence_len) diff --git a/graphql/core/type/directives.py b/graphql/core/type/directives.py deleted file mode 100644 index b789bee0..00000000 --- a/graphql/core/type/directives.py +++ /dev/null @@ -1,45 +0,0 @@ -from .definition import GraphQLArgument, GraphQLNonNull -from .scalars import GraphQLBoolean - - -class GraphQLDirective(object): - __slots__ = 'name', 'args', 'description', 'on_operation', 'on_fragment', 'on_field' - - def __init__(self, name, description=None, args=None, on_operation=False, on_fragment=False, on_field=False): - self.name = name - self.description = description - self.args = args or [] - self.on_operation = on_operation - self.on_fragment = on_fragment - self.on_field = on_field - - -def arg(name, *args, **kwargs): - a = GraphQLArgument(*args, **kwargs) - a.name = name - return a - - -GraphQLIncludeDirective = GraphQLDirective( - name='include', - args=[arg( - 'if', - type=GraphQLNonNull(GraphQLBoolean), - description='Directs the executor to include this field or fragment only when the `if` argument is true.', - )], - on_operation=False, - on_fragment=True, - on_field=True -) - -GraphQLSkipDirective = GraphQLDirective( - name='skip', - args=[arg( - 'if', - type=GraphQLNonNull(GraphQLBoolean), - description='Directs the executor to skip this field or fragment only when the `if` argument is true.', - )], - on_operation=False, - on_fragment=True, - on_field=True -) diff --git a/graphql/core/utils/build_ast_schema.py b/graphql/core/utils/build_ast_schema.py deleted file mode 100644 index f68684a9..00000000 --- a/graphql/core/utils/build_ast_schema.py +++ /dev/null @@ -1,172 +0,0 @@ -from collections import OrderedDict - -from ..language import ast -from ..type import (GraphQLArgument, GraphQLBoolean, GraphQLEnumType, - GraphQLEnumValue, GraphQLField, GraphQLFloat, GraphQLID, - GraphQLInputObjectField, GraphQLInputObjectType, - GraphQLInt, GraphQLInterfaceType, GraphQLList, - GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, - GraphQLSchema, GraphQLString, GraphQLUnionType) -from ..utils.value_from_ast import value_from_ast - - -def _build_wrapped_type(inner_type, input_type_ast): - if isinstance(input_type_ast, ast.ListType): - return GraphQLList(_build_wrapped_type(inner_type, input_type_ast.type)) - - if isinstance(input_type_ast, ast.NonNullType): - return GraphQLNonNull(_build_wrapped_type(inner_type, input_type_ast.type)) - - return inner_type - - -def _get_inner_type_name(type_ast): - if isinstance(type_ast, (ast.ListType, ast.NonNullType)): - return _get_inner_type_name(type_ast.type) - - return type_ast.name.value - - -def _false(*_): return False - - -def _none(*_): return None - - -def build_ast_schema(document, query_type_name, mutation_type_name=None, subscription_type_name=None): - assert isinstance(document, ast.Document), 'must pass in Document ast.' - assert query_type_name, 'must pass in query type' - - type_defs = [d for d in document.definitions if isinstance(d, ast.TypeDefinition)] - ast_map = {d.name.value: d for d in type_defs} - - if query_type_name not in ast_map: - raise Exception('Specified query type {} not found in document.'.format(query_type_name)) - - if mutation_type_name and mutation_type_name not in ast_map: - raise Exception('Specified mutation type {} not found in document.'.format(mutation_type_name)) - - if subscription_type_name and subscription_type_name not in ast_map: - raise Exception('Specified subscription type {} not found in document.'.format(subscription_type_name)) - - inner_type_map = OrderedDict([ - ('String', GraphQLString), - ('Int', GraphQLInt), - ('Float', GraphQLFloat), - ('Boolean', GraphQLBoolean), - ('ID', GraphQLID) - ]) - - def produce_type_def(type_ast): - type_name = _get_inner_type_name(type_ast) - if type_name in inner_type_map: - return _build_wrapped_type(inner_type_map[type_name], type_ast) - - if type_name not in ast_map: - raise Exception('Type {} not found in document.'.format(type_name)) - - inner_type_def = make_schema_def(ast_map[type_name]) - if not inner_type_def: - raise Exception('Nothing constructed for {}.'.format(type_name)) - - inner_type_map[type_name] = inner_type_def - return _build_wrapped_type(inner_type_def, type_ast) - - def make_type_def(definition): - return GraphQLObjectType( - name=definition.name.value, - fields=lambda: make_field_def_map(definition), - interfaces=make_implemented_interfaces(definition) - ) - - def make_field_def_map(definition): - return OrderedDict( - (f.name.value, GraphQLField( - type=produce_type_def(f.type), - args=make_input_values(f.arguments, GraphQLArgument) - )) - for f in definition.fields - ) - - def make_implemented_interfaces(definition): - return [produce_type_def(i) for i in definition.interfaces] - - def make_input_values(values, cls): - return OrderedDict( - (value.name.value, cls( - type=produce_type_def(value.type), - default_value=value_from_ast(value.default_value, produce_type_def(value.type)) - )) - for value in values - ) - - def make_interface_def(definition): - return GraphQLInterfaceType( - name=definition.name.value, - resolve_type=_none, - fields=lambda: make_field_def_map(definition) - ) - - def make_enum_def(definition): - return GraphQLEnumType( - name=definition.name.value, - values=OrderedDict( - (v.name.value, GraphQLEnumValue()) for v in definition.values - ) - ) - - def make_union_def(definition): - return GraphQLUnionType( - name=definition.name.value, - resolve_type=_none, - types=[produce_type_def(t) for t in definition.types] - ) - - def make_scalar_def(definition): - return GraphQLScalarType( - name=definition.name.value, - serialize=_none, - # Validation calls the parse functions to determine if a literal value is correct. - # Returning none, however would cause the scalar to fail validation. Returning false, - # will cause them to pass. - parse_literal=_false, - parse_value=_false - ) - - def make_input_object_def(definition): - return GraphQLInputObjectType( - name=definition.name.value, - fields=make_input_values(definition.fields, GraphQLInputObjectField) - ) - - _schema_def_handlers = { - ast.ObjectTypeDefinition: make_type_def, - ast.InterfaceTypeDefinition: make_interface_def, - ast.EnumTypeDefinition: make_enum_def, - ast.UnionTypeDefinition: make_union_def, - ast.ScalarTypeDefinition: make_scalar_def, - ast.InputObjectTypeDefinition: make_input_object_def - } - - def make_schema_def(definition): - if not definition: - raise Exception('definition must be defined.') - - handler = _schema_def_handlers.get(type(definition)) - if not handler: - raise Exception('{} not supported.'.format(type(definition).__name__)) - - return handler(definition) - - for definition in document.definitions: - produce_type_def(definition) - - schema_kwargs = {'query': produce_type_def(ast_map[query_type_name])} - - if mutation_type_name: - schema_kwargs['mutation'] = produce_type_def(ast_map[mutation_type_name]) - - if subscription_type_name: - schema_kwargs['subscription'] = produce_type_def(ast_map[subscription_type_name]) - - return GraphQLSchema(**schema_kwargs) diff --git a/graphql/core/utils/type_comparators.py b/graphql/core/utils/type_comparators.py deleted file mode 100644 index 0fa358b4..00000000 --- a/graphql/core/utils/type_comparators.py +++ /dev/null @@ -1,56 +0,0 @@ -from ..type.definition import (GraphQLInterfaceType, GraphQLList, GraphQLNonNull, - GraphQLObjectType, GraphQLUnionType, - is_abstract_type) - - -def is_equal_type(type_a, type_b): - if type_a is type_b: - return True - - if isinstance(type_a, GraphQLNonNull) and isinstance(type_b, GraphQLNonNull): - return is_equal_type(type_a.of_type, type_b.of_type) - - if isinstance(type_a, GraphQLList) and isinstance(type_b, GraphQLList): - return is_equal_type(type_a.of_type, type_b.of_type) - - return False - - -def is_type_sub_type_of(maybe_subtype, super_type): - if maybe_subtype is super_type: - return True - - if isinstance(super_type, GraphQLNonNull): - if isinstance(maybe_subtype, GraphQLNonNull): - return is_type_sub_type_of(maybe_subtype.of_type, super_type.of_type) - return False - elif isinstance(maybe_subtype, GraphQLNonNull): - return is_type_sub_type_of(maybe_subtype.of_type, super_type) - - if isinstance(super_type, GraphQLList): - if isinstance(maybe_subtype, GraphQLList): - return is_type_sub_type_of(maybe_subtype.of_type, super_type.of_type) - return False - elif isinstance(maybe_subtype, GraphQLList): - return False - - if is_abstract_type(super_type) and isinstance(maybe_subtype, - GraphQLObjectType) and super_type.is_possible_type(maybe_subtype): - return True - - return False - - -def do_types_overlap(t1, t2): - if t1 == t2: - return True - if isinstance(t1, GraphQLObjectType): - if isinstance(t2, GraphQLObjectType): - return False - return t1 in t2.get_possible_types() - if isinstance(t1, GraphQLInterfaceType) or isinstance(t1, GraphQLUnionType): - if isinstance(t2, GraphQLObjectType): - return t2 in t1.get_possible_types() - - t1_type_names = {possible_type.name: possible_type for possible_type in t1.get_possible_types()} - return any(t.name in t1_type_names for t in t2.get_possible_types()) diff --git a/graphql/core/validation/__init__.py b/graphql/core/validation/__init__.py deleted file mode 100644 index 7d1e3701..00000000 --- a/graphql/core/validation/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from ..language.visitor import ParallelVisitor, TypeInfoVisitor, visit -from ..type import GraphQLSchema -from ..utils.type_info import TypeInfo -from .context import ValidationContext -from .rules import specified_rules - - -def validate(schema, ast, rules=specified_rules): - assert schema, 'Must provide schema' - assert ast, 'Must provide document' - assert isinstance(schema, GraphQLSchema) - type_info = TypeInfo(schema) - return visit_using_rules(schema, type_info, ast, rules) - - -def visit_using_rules(schema, type_info, ast, rules): - context = ValidationContext(schema, ast, type_info) - visitors = [rule(context) for rule in rules] - visit(ast, TypeInfoVisitor(type_info, ParallelVisitor(visitors))) - return context.get_errors() diff --git a/graphql/error/__init__.py b/graphql/error/__init__.py new file mode 100644 index 00000000..fdcf8149 --- /dev/null +++ b/graphql/error/__init__.py @@ -0,0 +1,6 @@ +from .base import GraphQLError +from .located_error import GraphQLLocatedError +from .syntax_error import GraphQLSyntaxError +from .format_error import format_error + +__all__ = ['GraphQLError', 'GraphQLLocatedError', 'GraphQLSyntaxError', 'format_error'] diff --git a/graphql/core/error.py b/graphql/error/base.py similarity index 52% rename from graphql/core/error.py rename to graphql/error/base.py index 9308eb42..ccc75e24 100644 --- a/graphql/core/error.py +++ b/graphql/error/base.py @@ -1,18 +1,14 @@ -from .language.location import get_location +from ..language.location import get_location -class Error(Exception): - pass - - -class GraphQLError(Error): - __slots__ = 'message', 'nodes', 'stack', '_source', '_positions' +class GraphQLError(Exception): + __slots__ = 'message', 'nodes', 'stack', 'original_error', '_source', '_positions' def __init__(self, message, nodes=None, stack=None, source=None, positions=None): super(GraphQLError, self).__init__(message) - self.message = message or 'An unknown error occurred.' + self.message = message self.nodes = nodes - self.stack = stack or message + self.stack = stack self._source = source self._positions = positions @@ -35,18 +31,6 @@ def positions(self): @property def locations(self): - if self.positions and self.source: - return [get_location(self.source, pos) for pos in self.positions] - - -def format_error(error): - formatted_error = { - 'message': error.message, - } - if error.locations is not None: - formatted_error['locations'] = [ - {'line': loc.line, 'column': loc.column} - for loc in error.locations - ] - - return formatted_error + source = self.source + if self.positions and source: + return [get_location(source, pos) for pos in self.positions] diff --git a/graphql/error/format_error.py b/graphql/error/format_error.py new file mode 100644 index 00000000..04095503 --- /dev/null +++ b/graphql/error/format_error.py @@ -0,0 +1,11 @@ +def format_error(error): + formatted_error = { + 'message': error.message, + } + if error.locations is not None: + formatted_error['locations'] = [ + {'line': loc.line, 'column': loc.column} + for loc in error.locations + ] + + return formatted_error diff --git a/graphql/error/located_error.py b/graphql/error/located_error.py new file mode 100644 index 00000000..c095ea17 --- /dev/null +++ b/graphql/error/located_error.py @@ -0,0 +1,26 @@ +import sys + +from .base import GraphQLError + +__all__ = ['GraphQLLocatedError'] + + +class GraphQLLocatedError(GraphQLError): + + def __init__(self, nodes, original_error=None): + if original_error: + message = str(original_error) + else: + message = 'An unknown error occurred.' + + if isinstance(original_error, GraphQLError): + stack = original_error.stack + else: + stack = sys.exc_info()[2] + + super(GraphQLLocatedError, self).__init__( + message=message, + nodes=nodes, + stack=stack + ) + self.original_error = original_error diff --git a/graphql/core/language/error.py b/graphql/error/syntax_error.py similarity index 82% rename from graphql/core/language/error.py rename to graphql/error/syntax_error.py index 755e9af5..16eb487f 100644 --- a/graphql/core/language/error.py +++ b/graphql/error/syntax_error.py @@ -1,14 +1,14 @@ -from ..error import GraphQLError -from .location import get_location +from ..language.location import get_location +from .base import GraphQLError -__all__ = ['LanguageError'] +__all__ = ['GraphQLSyntaxError'] -class LanguageError(GraphQLError): +class GraphQLSyntaxError(GraphQLError): def __init__(self, source, position, description): location = get_location(source, position) - super(LanguageError, self).__init__( + super(GraphQLSyntaxError, self).__init__( message=u'Syntax Error {} ({}:{}) {}\n\n{}'.format( source.name, location.line, diff --git a/graphql/execution/__init__.py b/graphql/execution/__init__.py new file mode 100644 index 00000000..2d5e49ee --- /dev/null +++ b/graphql/execution/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" +Terminology + +"Definitions" are the generic name for top-level statements in the document. +Examples of this include: +1) Operations (such as a query) +2) Fragments + +"Operations" are a generic name for requests in the document. +Examples of this include: +1) query, +2) mutation + +"Selections" are the statements that can appear legally and at +single level of the query. These include: +1) field references e.g "a" +2) fragment "spreads" e.g. "...c" +3) inline fragment "spreads" e.g. "...on Type { a }" +""" +from .executor import execute +from .base import ExecutionResult + + +__all__ = ['execute', 'ExecutionResult'] diff --git a/graphql/core/execution/base.py b/graphql/execution/base.py similarity index 86% rename from graphql/core/execution/base.py rename to graphql/execution/base.py index 66c6a269..fd6648c2 100644 --- a/graphql/core/execution/base.py +++ b/graphql/execution/base.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from ..error import GraphQLError from ..language import ast -from ..pyutils.defer import DeferredException from ..type.definition import GraphQLInterfaceType, GraphQLUnionType from ..type.directives import GraphQLIncludeDirective, GraphQLSkipDirective from ..type.introspection import (SchemaMetaFieldDef, TypeMetaFieldDef, @@ -18,10 +17,10 @@ class ExecutionContext(object): Namely, schema of the type system that is currently executing, and the fragments defined in the query document""" - __slots__ = 'schema', 'fragments', 'root', 'operation', 'variables', 'errors', 'request_context', \ - 'argument_values_cache' + __slots__ = 'schema', 'fragments', 'root_value', 'operation', 'variable_values', 'errors', 'context_value', \ + 'argument_values_cache', 'executor' - def __init__(self, schema, root, document_ast, operation_name, args, request_context): + def __init__(self, schema, document_ast, root_value, context_value, variable_values, operation_name, executor): """Constructs a ExecutionContext object from the arguments passed to execute, which we will pass throughout the other execution methods.""" @@ -53,16 +52,17 @@ def __init__(self, schema, root, document_ast, operation_name, args, request_con else: raise GraphQLError('Must provide an operation.') - variables = get_variable_values(schema, operation.variable_definitions or [], args) + variable_values = get_variable_values(schema, operation.variable_definitions or [], variable_values) self.schema = schema self.fragments = fragments - self.root = root + self.root_value = root_value self.operation = operation - self.variables = variables + self.variable_values = variable_values self.errors = errors - self.request_context = request_context + self.context_value = context_value self.argument_values_cache = {} + self.executor = executor def get_argument_values(self, field_def, field_ast): k = field_def, field_ast @@ -70,7 +70,7 @@ def get_argument_values(self, field_def, field_ast): if not result: result = self.argument_values_cache[k] = get_argument_values(field_def.args, field_ast.arguments, - self.variables) + self.variable_values) return result @@ -84,12 +84,6 @@ class ExecutionResult(object): def __init__(self, data=None, errors=None, invalid=False): self.data = data - if errors: - errors = [ - error.value if isinstance(error, DeferredException) else error - for error in errors - ] - self.errors = errors if invalid: @@ -190,6 +184,7 @@ def collect_fields(ctx, runtime_type, selection_set, fields, prev_fragment_names def should_include_node(ctx, directives): """Determines if a field should be included based on the @include and @skip directives, where @skip has higher precidence than @include.""" + # TODO: Refactor based on latest code if directives: skip_ast = None @@ -202,9 +197,10 @@ def should_include_node(ctx, directives): args = get_argument_values( GraphQLSkipDirective.args, skip_ast.arguments, - ctx.variables, + ctx.variable_values, ) - return not args.get('if') + if args.get('if') is True: + return False include_ast = None @@ -217,10 +213,11 @@ def should_include_node(ctx, directives): args = get_argument_values( GraphQLIncludeDirective.args, include_ast.arguments, - ctx.variables, + ctx.variable_values, ) - return bool(args.get('if')) + if args.get('if') is False: + return False return True @@ -235,7 +232,7 @@ def does_fragment_condition_match(ctx, fragment, type_): return True if isinstance(conditional_type, (GraphQLInterfaceType, GraphQLUnionType)): - return conditional_type.is_possible_type(type_) + return ctx.schema.is_possible_type(conditional_type, type_) return False @@ -249,39 +246,20 @@ def get_field_entry_key(node): class ResolveInfo(object): - def __init__(self, field_name, field_asts, return_type, parent_type, context): + def __init__(self, field_name, field_asts, return_type, parent_type, + schema, fragments, root_value, operation, variable_values): self.field_name = field_name self.field_asts = field_asts self.return_type = return_type self.parent_type = parent_type - self.context = context - - @property - def schema(self): - return self.context.schema - - @property - def fragments(self): - return self.context.fragments - - @property - def root_value(self): - return self.context.root - - @property - def operation(self): - return self.context.operation - - @property - def variable_values(self): - return self.context.variables - - @property - def request_context(self): - return self.context.request_context + self.schema = schema + self.fragments = fragments + self.root_value = root_value + self.operation = operation + self.variable_values = variable_values -def default_resolve_fn(source, args, info): +def default_resolve_fn(source, args, context, info): """If a resolve function is not given, then a default resolve behavior is used which takes the property of the source object of the same name as the field and returns it as the result, or if it's a function, returns the result of calling that function.""" name = info.field_name diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py new file mode 100644 index 00000000..2402bf63 --- /dev/null +++ b/graphql/execution/executor.py @@ -0,0 +1,371 @@ +import collections +import functools +import logging + +from promise import Promise, is_thenable, promise_for_dict, promisify + +from ..error import GraphQLError, GraphQLLocatedError +from ..pyutils.default_ordered_dict import DefaultOrderedDict +from ..type import (GraphQLEnumType, GraphQLInterfaceType, GraphQLList, + GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, + GraphQLSchema, GraphQLUnionType) +from .base import (ExecutionContext, ExecutionResult, ResolveInfo, Undefined, + collect_fields, default_resolve_fn, get_field_def, + get_operation_root_type) +from .executors.sync import SyncExecutor + +logger = logging.getLogger(__name__) + + +def execute(schema, document_ast, root_value=None, context_value=None, + variable_values=None, operation_name=None, executor=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 executor is None: + executor = SyncExecutor() + + context = ExecutionContext( + schema, + document_ast, + root_value, + context_value, + variable_values, + operation_name, + executor + ) + + 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) + + p = Promise(executor).catch(on_rejected).then(on_resolve) + context.executor.wait_until_finished() + return p.value + + +def execute_operation(exe_context, operation, root_value): + type = get_operation_root_type(exe_context.schema, operation) + fields = collect_fields( + exe_context, + type, + operation.selection_set, + DefaultOrderedDict(list), + set() + ) + + if operation.operation == 'mutation': + return execute_fields_serially(exe_context, type, root_value, fields) + + return execute_fields(exe_context, type, root_value, fields) + + +def execute_fields_serially(exe_context, parent_type, source_value, fields): + def execute_field_callback(results, response_name): + field_asts = fields[response_name] + result = resolve_field( + exe_context, + parent_type, + source_value, + field_asts + ) + if result is Undefined: + return results + + if is_thenable(result): + def collect_result(resolved_result): + results[response_name] = resolved_result + return results + + return promisify(result).then(collect_result, None) + + results[response_name] = result + return results + + def execute_field(prev_promise, response_name): + return prev_promise.then(lambda results: execute_field_callback(results, response_name)) + + return functools.reduce(execute_field, fields.keys(), Promise.resolve(collections.OrderedDict())) + + +def execute_fields(exe_context, parent_type, source_value, fields): + contains_promise = False + + final_results = collections.OrderedDict() + + for response_name, field_asts in fields.items(): + result = resolve_field(exe_context, parent_type, source_value, field_asts) + if result is Undefined: + continue + + final_results[response_name] = result + if is_thenable(result): + contains_promise = True + + if not contains_promise: + return final_results + + return promise_for_dict(final_results) + + +def resolve_field(exe_context, parent_type, source, field_asts): + field_ast = field_asts[0] + field_name = field_ast.name.value + + field_def = get_field_def(exe_context.schema, parent_type, field_name) + if not field_def: + return Undefined + + return_type = field_def.type + resolve_fn = field_def.resolver or default_resolve_fn + + # Build a dict of arguments from the field.arguments AST, using the variables scope to + # fulfill any variable references. + args = exe_context.get_argument_values(field_def, field_ast) + + # The resolve function's optional third argument is a context value that + # is provided to every resolve function within an execution. It is commonly + # used to represent an authenticated user, or request-specific caches. + context = exe_context.context_value + + # The resolve function's optional third argument is a collection of + # information about the current execution state. + info = ResolveInfo( + field_name, + field_asts, + return_type, + parent_type, + schema=exe_context.schema, + fragments=exe_context.fragments, + root_value=exe_context.root_value, + operation=exe_context.operation, + variable_values=exe_context.variable_values, + ) + + executor = exe_context.executor + result = resolve_or_error(resolve_fn, source, args, context, info, executor) + + return complete_value_catching_error( + exe_context, + return_type, + field_asts, + info, + result + ) + + +def resolve_or_error(resolve_fn, source, args, context, info, executor): + try: + return executor.execute(resolve_fn, source, args, context, info) + except Exception as e: + logger.exception("An error occurred while resolving field {}.{}".format( + info.parent_type.name, info.field_name + )) + return e + + +def complete_value_catching_error(exe_context, return_type, field_asts, info, result): + # If the field type is non-nullable, then it is resolved without any + # protection from errors. + if isinstance(return_type, GraphQLNonNull): + return complete_value(exe_context, return_type, field_asts, info, result) + + # Otherwise, error protection is applied, logging the error and + # resolving a null value for this field if one is encountered. + try: + completed = complete_value(exe_context, return_type, field_asts, info, result) + if is_thenable(completed): + def handle_error(error): + exe_context.errors.append(error) + return Promise.fulfilled(None) + + return promisify(completed).then(None, handle_error) + + return completed + except Exception as e: + exe_context.errors.append(e) + return None + + +def complete_value(exe_context, return_type, field_asts, info, result): + """ + Implements the instructions for completeValue as defined in the + "Field entries" section of the spec. + + If the field type is Non-Null, then this recursively completes the value for the inner type. It throws a field + error if that completion returns null, as per the "Nullability" section of the spec. + + If the field type is a List, then this recursively completes the value for the inner type on each item in the + list. + + If the field type is a Scalar or Enum, ensures the completed value is a legal value of the type by calling the + `serialize` method of GraphQL type definition. + + If the field is an abstract type, determine the runtime type of the value and then complete based on that type. + + Otherwise, the field type expects a sub-selection set, and will complete the value by evaluating all + sub-selections. + """ + # 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( + lambda resolved: complete_value( + exe_context, + return_type, + field_asts, + info, + resolved + ), + lambda error: Promise.rejected(GraphQLLocatedError(field_asts, original_error=error)) + ) + + if isinstance(result, Exception): + raise GraphQLLocatedError(field_asts, original_error=result) + + if isinstance(return_type, GraphQLNonNull): + completed = complete_value( + exe_context, return_type.of_type, field_asts, info, result + ) + if completed is None: + raise GraphQLError( + 'Cannot return null for non-nullable field {}.{}.'.format(info.parent_type, info.field_name), + field_asts + ) + + return completed + + # If result is null-like, return null. + if result is None: + return None + + # If field type is List, complete each item in the list with the inner type + if isinstance(return_type, GraphQLList): + return complete_list_value(exe_context, return_type, field_asts, info, result) + + # If field type is Scalar or Enum, serialize to a valid value, returning null if coercion is not possible. + if isinstance(return_type, (GraphQLScalarType, GraphQLEnumType)): + return complete_leaf_value(return_type, result) + + if isinstance(return_type, (GraphQLInterfaceType, GraphQLUnionType)): + return complete_abstract_value(exe_context, return_type, field_asts, info, result) + + if isinstance(return_type, GraphQLObjectType): + return complete_object_value(exe_context, return_type, field_asts, info, result) + + assert False, u'Cannot complete value of unexpected type "{}".'.format(return_type) + + +def complete_list_value(exe_context, return_type, field_asts, info, result): + """ + Complete a list value by completing each item in the list with the inner type + """ + assert isinstance(result, collections.Iterable), \ + ('User Error: expected iterable, but did not find one ' + + 'for field {}.{}.').format(info.parent_type, info.field_name) + + item_type = return_type.of_type + completed_results = [] + contains_promise = False + for item in result: + completed_item = complete_value_catching_error(exe_context, item_type, field_asts, info, item) + if not contains_promise and is_thenable(completed_item): + contains_promise = True + + completed_results.append(completed_item) + + return Promise.all(completed_results) if contains_promise else completed_results + + +def complete_leaf_value(return_type, result): + """ + Complete a Scalar or Enum by serializing to a valid value, returning null if serialization is not possible. + """ + serialize = getattr(return_type, 'serialize', None) + assert serialize, 'Missing serialize method on type' + + serialized_result = serialize(result) + + if serialized_result is None: + return None + + return serialized_result + + +def complete_abstract_value(exe_context, return_type, field_asts, info, result): + """ + Complete an value of an abstract type by determining the runtime type of that value, then completing based + on that type. + """ + runtime_type = None + + # Field type must be Object, Interface or Union and expect sub-selections. + if isinstance(return_type, (GraphQLInterfaceType, GraphQLUnionType)): + if return_type.resolve_type: + runtime_type = return_type.resolve_type(result, exe_context.context_value, info) + else: + runtime_type = get_default_resolve_type_fn(result, exe_context.context_value, info, return_type) + + assert runtime_type, ( + 'Could not determine runtime type of value "{}" for field {}.{}.'.format( + result, + info.parent_type, + info.field_name + )) + + assert isinstance(runtime_type, GraphQLObjectType), ( + '{}.resolveType must return an instance of GraphQLObjectType ' + + 'for field {}.{}, received "{}".'.format( + return_type, + info.parent_type, + info.field_name, + result, + )) + + if not exe_context.schema.is_possible_type(return_type, runtime_type): + raise GraphQLError( + u'Runtime Object type "{}" is not a possible type for "{}".'.format(runtime_type, return_type), + field_asts + ) + + return complete_object_value(exe_context, runtime_type, field_asts, info, result) + + +def get_default_resolve_type_fn(value, context, info, abstract_type): + possible_types = info.schema.get_possible_types(abstract_type) + for type in possible_types: + if callable(type.is_type_of) and type.is_type_of(value, context, info): + return type + + +def complete_object_value(exe_context, return_type, field_asts, info, result): + """ + Complete an Object value by evaluating all sub-selections. + """ + if return_type.is_type_of and not return_type.is_type_of(result, exe_context.context_value, info): + raise GraphQLError( + u'Expected value of type "{}" but got: {}.'.format(return_type, type(result).__name__), + field_asts + ) + + # Collect sub-fields to execute to complete this value. + 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( + exe_context, return_type, selection_set, + subfield_asts, visited_fragment_names + ) + + return execute_fields(exe_context, return_type, result, subfield_asts) diff --git a/graphql/core/language/__init__.py b/graphql/execution/executors/__init__.py similarity index 100% rename from graphql/core/language/__init__.py rename to graphql/execution/executors/__init__.py diff --git a/graphql/execution/executors/asyncio.py b/graphql/execution/executors/asyncio.py new file mode 100644 index 00000000..0652e37a --- /dev/null +++ b/graphql/execution/executors/asyncio.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import + +from asyncio import Future, ensure_future, get_event_loop, iscoroutine, wait + + +class AsyncioExecutor(object): + + def __init__(self): + self.loop = get_event_loop() + self.futures = [] + + def wait_until_finished(self): + self.loop.run_until_complete(wait(self.futures)) + + def execute(self, fn, *args, **kwargs): + result = fn(*args, **kwargs) + if isinstance(result, Future) or iscoroutine(result): + future = ensure_future(result) + self.futures.append(future) + return future + return result diff --git a/graphql/execution/executors/gevent.py b/graphql/execution/executors/gevent.py new file mode 100644 index 00000000..8df4fd0c --- /dev/null +++ b/graphql/execution/executors/gevent.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import + +import gevent +from promise import Promise + +from .utils import process + + +class GeventExecutor(object): + + def __init__(self): + self.jobs = [] + + def wait_until_finished(self): + [j.join() for j in self.jobs] + # gevent.joinall(self.jobs) + + def execute(self, fn, *args, **kwargs): + promise = Promise() + job = gevent.spawn(process, promise, fn, args, kwargs) + self.jobs.append(job) + return promise diff --git a/graphql/execution/executors/process.py b/graphql/execution/executors/process.py new file mode 100644 index 00000000..9cb8e877 --- /dev/null +++ b/graphql/execution/executors/process.py @@ -0,0 +1,32 @@ +from multiprocessing import Process, Queue + +from promise import Promise + +from .utils import process + + +def queue_process(q): + promise, fn, args, kwargs = q.get() + process(promise, fn, args, kwargs) + + +class ProcessExecutor(object): + + def __init__(self): + self.processes = [] + self.q = Queue() + + def wait_until_finished(self): + for _process in self.processes: + _process.join() + self.q.close() + self.q.join_thread() + + def execute(self, fn, *args, **kwargs): + promise = Promise() + + self.q.put([promise, fn, args, kwargs], False) + _process = Process(target=queue_process, args=(self.q)) + _process.start() + self.processes.append(_process) + return promise diff --git a/graphql/execution/executors/sync.py b/graphql/execution/executors/sync.py new file mode 100644 index 00000000..85f8471b --- /dev/null +++ b/graphql/execution/executors/sync.py @@ -0,0 +1,7 @@ +class SyncExecutor(object): + + def wait_until_finished(self): + pass + + def execute(self, fn, *args, **kwargs): + return fn(*args, **kwargs) diff --git a/graphql/execution/executors/thread.py b/graphql/execution/executors/thread.py new file mode 100644 index 00000000..610c282b --- /dev/null +++ b/graphql/execution/executors/thread.py @@ -0,0 +1,35 @@ +from multiprocessing.pool import ThreadPool +from threading import Thread + +from promise import Promise + +from .utils import process + + +class ThreadExecutor(object): + + pool = None + + def __init__(self, pool=False): + self.threads = [] + if pool: + self.execute = self.execute_in_pool + self.pool = ThreadPool(processes=pool) + else: + self.execute = self.execute_in_thread + + def wait_until_finished(self): + for thread in self.threads: + thread.join() + + def execute_in_thread(self, fn, *args, **kwargs): + promise = Promise() + thread = Thread(target=process, args=(promise, fn, args, kwargs)) + thread.start() + self.threads.append(thread) + return promise + + def execute_in_pool(self, fn, *args, **kwargs): + promise = Promise() + self.pool.map(lambda input: process(*input), [(promise, fn, args, kwargs)]) + return promise diff --git a/graphql/execution/executors/utils.py b/graphql/execution/executors/utils.py new file mode 100644 index 00000000..79b67cbe --- /dev/null +++ b/graphql/execution/executors/utils.py @@ -0,0 +1,6 @@ +def process(p, f, args, kwargs): + try: + val = f(*args, **kwargs) + p.fulfill(val) + except Exception as e: + p.reject(e) diff --git a/graphql/core/pyutils/__init__.py b/graphql/execution/tests/__init__.py similarity index 100% rename from graphql/core/pyutils/__init__.py rename to graphql/execution/tests/__init__.py diff --git a/tests/core_execution/test_abstract.py b/graphql/execution/tests/test_abstract.py similarity index 92% rename from tests/core_execution/test_abstract.py rename to graphql/execution/tests/test_abstract.py index 9bf9021b..d7ca41f4 100644 --- a/tests/core_execution/test_abstract.py +++ b/graphql/execution/tests/test_abstract.py @@ -1,8 +1,8 @@ -from graphql.core import graphql -from graphql.core.type import GraphQLBoolean, GraphQLSchema, GraphQLString -from graphql.core.type.definition import (GraphQLField, GraphQLInterfaceType, - GraphQLList, GraphQLObjectType, - GraphQLUnionType) +from graphql import graphql +from graphql.type import GraphQLBoolean, GraphQLSchema, GraphQLString +from graphql.type.definition import (GraphQLField, GraphQLInterfaceType, + GraphQLList, GraphQLObjectType, + GraphQLUnionType) class Dog(object): @@ -25,11 +25,11 @@ def __init__(self, name): self.name = name -is_type_of = lambda type: lambda obj, info: isinstance(obj, type) +is_type_of = lambda type: lambda obj, context, info: isinstance(obj, type) def make_type_resolver(types): - def resolve_type(obj, info): + def resolve_type(obj, context, info): if callable(types): t = types() else: @@ -81,7 +81,8 @@ def test_is_type_of_used_to_resolve_runtime_type_for_interface(): resolver=lambda *_: [Dog('Odie', True), Cat('Garfield', False)] ) } - ) + ), + types=[CatType, DogType] ) query = ''' @@ -136,7 +137,8 @@ def test_is_type_of_used_to_resolve_runtime_type_for_union(): resolver=lambda *_: [Dog('Odie', True), Cat('Garfield', False)] ) } - ) + ), + types=[CatType, DogType] ) query = ''' @@ -206,7 +208,8 @@ def test_resolve_type_on_interface_yields_useful_error(): resolver=lambda *_: [Dog('Odie', True), Cat('Garfield', False), Human('Jon')] ) } - ) + ), + types=[DogType, CatType] ) query = ''' diff --git a/tests/core_execution/test_directives.py b/graphql/execution/tests/test_directives.py similarity index 84% rename from tests/core_execution/test_directives.py rename to graphql/execution/tests/test_directives.py index dac626bb..0b42ebe2 100644 --- a/tests/core_execution/test_directives.py +++ b/graphql/execution/tests/test_directives.py @@ -1,7 +1,7 @@ -from graphql.core.execution import execute -from graphql.core.language.parser import parse -from graphql.core.type import (GraphQLField, GraphQLObjectType, GraphQLSchema, - GraphQLString) +from graphql.execution import execute +from graphql.language.parser import parse +from graphql.type import (GraphQLField, GraphQLObjectType, GraphQLSchema, + GraphQLString) schema = GraphQLSchema( query=GraphQLObjectType( @@ -20,7 +20,7 @@ class Data(object): def execute_test_query(doc): - return execute(schema, Data, parse(doc)) + return execute(schema, parse(doc), Data) def test_basic_query_works(): @@ -181,51 +181,6 @@ def test_skip_true_omits_inline_fragment(): assert result.data == {'a': 'a'} -def test_if_false_omits_fragment(): - q = ''' - query Q { - a - ...Frag - } - fragment Frag 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_fragment(): - q = ''' - query Q { - a - ...Frag - } - fragment Frag 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_fragment(): - q = ''' - query Q { - a - ...Frag - } - fragment Frag 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_fragment(): q = ''' query Q { @@ -295,3 +250,21 @@ def test_include_on_inline_anonymous_fragment_does_not_omit_field(): 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/tests/core_execution/test_executor_schema.py b/graphql/execution/tests/test_execute_schema.py similarity index 92% rename from tests/core_execution/test_executor_schema.py rename to graphql/execution/tests/test_execute_schema.py index cc5def57..a5c2a299 100644 --- a/tests/core_execution/test_executor_schema.py +++ b/graphql/execution/tests/test_execute_schema.py @@ -1,9 +1,8 @@ -from graphql.core.execution import execute -from graphql.core.language.parser import parse -from graphql.core.type import (GraphQLArgument, GraphQLBoolean, GraphQLField, - GraphQLID, GraphQLInt, GraphQLList, - GraphQLNonNull, GraphQLObjectType, - GraphQLSchema, GraphQLString) +from graphql.execution import execute +from graphql.language.parser import parse +from graphql.type import (GraphQLArgument, GraphQLBoolean, GraphQLField, + GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, + GraphQLObjectType, GraphQLSchema, GraphQLString) def test_executes_using_a_schema(): @@ -111,7 +110,7 @@ def __init__(self, uid, width, height): # Note: this is intentionally not validating to ensure appropriate # behavior occurs when executing an invalid query. - result = execute(BlogSchema, None, parse(request)) + result = execute(BlogSchema, parse(request)) assert not result.errors assert result.data == \ { diff --git a/tests/core_execution/test_executor.py b/graphql/execution/tests/test_executor.py similarity index 71% rename from tests/core_execution/test_executor.py rename to graphql/execution/tests/test_executor.py index 2e252694..6ccda283 100644 --- a/tests/core_execution/test_executor.py +++ b/graphql/execution/tests/test_executor.py @@ -1,16 +1,13 @@ import json -from collections import OrderedDict from pytest import raises -from graphql.core.error import GraphQLError -from graphql.core.execution import Executor, execute -from graphql.core.execution.middlewares.sync import \ - SynchronousExecutionMiddleware -from graphql.core.language.parser import parse -from graphql.core.type import (GraphQLArgument, GraphQLBoolean, GraphQLField, - GraphQLInt, GraphQLList, GraphQLObjectType, - GraphQLSchema, GraphQLString) +from graphql.error import GraphQLError +from graphql.execution import execute +from graphql.language.parser import parse +from graphql.type import (GraphQLArgument, GraphQLBoolean, GraphQLField, + GraphQLInt, GraphQLList, GraphQLObjectType, + GraphQLSchema, GraphQLString) def test_executes_arbitary_code(): @@ -114,7 +111,7 @@ def deeper(self): schema = GraphQLSchema(query=DataType) - result = execute(schema, Data(), ast, 'Example', {'size': 100}) + result = execute(schema, ast, Data(), operation_name='Example', variable_values={'size': 100}) assert not result.errors assert result.data == expected @@ -145,7 +142,7 @@ def test_merges_parallel_fragments(): }) schema = GraphQLSchema(query=Type) - result = execute(schema, None, ast) + result = execute(schema, ast) assert not result.errors assert result.data == \ { @@ -161,7 +158,7 @@ def test_merges_parallel_fragments(): } -def test_threads_context_correctly(): +def test_threads_root_value_context_correctly(): doc = 'query Example { a }' class Data(object): @@ -169,8 +166,8 @@ class Data(object): ast = parse(doc) - def resolver(context, *_): - assert context.context_thing == 'thing' + def resolver(root_value, *_): + assert root_value.context_thing == 'thing' resolver.got_here = True resolver.got_here = False @@ -179,7 +176,7 @@ def resolver(context, *_): 'a': GraphQLField(GraphQLString, resolver=resolver) }) - result = execute(GraphQLSchema(Type), Data(), ast, 'Example', {}) + result = execute(GraphQLSchema(Type), ast, Data(), operation_name='Example') assert not result.errors assert resolver.got_here @@ -210,7 +207,7 @@ def resolver(_, args, *_args): resolver=resolver), }) - result = execute(GraphQLSchema(Type), None, doc_ast, 'Example', {}) + result = execute(GraphQLSchema(Type), doc_ast, None, operation_name='Example') assert not result.errors assert resolver.got_here @@ -236,14 +233,14 @@ def error(self): 'error': GraphQLField(GraphQLString), }) - result = execute(GraphQLSchema(Type), Data(), doc_ast) + result = execute(GraphQLSchema(Type), doc_ast, Data()) assert result.data == {'ok': 'ok', 'error': None} assert len(result.errors) == 1 assert result.errors[0].message == 'Error getting error' # TODO: check error location -def test_uses_the_inline_operation_if_no_operation_is_provided(): +def test_uses_the_inline_operation_if_no_operation_name_is_provided(): doc = '{ a }' class Data(object): @@ -253,12 +250,12 @@ class Data(object): Type = GraphQLObjectType('Type', { 'a': GraphQLField(GraphQLString) }) - result = execute(GraphQLSchema(Type), Data(), ast) + result = execute(GraphQLSchema(Type), ast, Data()) assert not result.errors assert result.data == {'a': 'b'} -def test_uses_the_only_operation_if_no_operation_is_provided(): +def test_uses_the_only_operation_if_no_operation_name_is_provided(): doc = 'query Example { a }' class Data(object): @@ -268,12 +265,57 @@ class Data(object): Type = GraphQLObjectType('Type', { 'a': GraphQLField(GraphQLString) }) - result = execute(GraphQLSchema(Type), Data(), ast) + result = execute(GraphQLSchema(Type), ast, Data()) assert not result.errors assert result.data == {'a': 'b'} -def test_raises_the_inline_operation_if_no_operation_is_provided(): +def test_uses_the_named_operation_if_operation_name_is_provided(): + doc = 'query Example { first: a } query OtherExample { second: a }' + + class Data(object): + a = 'b' + + ast = parse(doc) + Type = GraphQLObjectType('Type', { + 'a': GraphQLField(GraphQLString) + }) + result = execute(GraphQLSchema(Type), ast, Data(), operation_name='OtherExample') + assert not result.errors + assert result.data == {'second': 'b'} + + +def test_uses_the_named_operation_if_operation_name_is_provided(): + doc = 'query Example { first: a } query OtherExample { second: a }' + + class Data(object): + a = 'b' + + ast = parse(doc) + Type = GraphQLObjectType('Type', { + 'a': GraphQLField(GraphQLString) + }) + result = execute(GraphQLSchema(Type), ast, Data(), operation_name='OtherExample') + assert not result.errors + assert result.data == {'second': 'b'} + + +def test_raises_if_no_operation_is_provided(): + doc = 'fragment Example on Type { a }' + + class Data(object): + a = 'b' + + ast = parse(doc) + Type = GraphQLObjectType('Type', { + 'a': GraphQLField(GraphQLString) + }) + with raises(GraphQLError) as excinfo: + execute(GraphQLSchema(Type), ast, Data()) + assert 'Must provide an operation.' == str(excinfo.value) + + +def test_raises_if_no_operation_name_is_provided_with_multiple_operations(): doc = 'query Example { a } query OtherExample { a }' class Data(object): @@ -284,8 +326,23 @@ class Data(object): 'a': GraphQLField(GraphQLString) }) with raises(GraphQLError) as excinfo: - execute(GraphQLSchema(Type), Data(), ast) - assert 'Must provide operation name if query contains multiple operations' in str(excinfo.value) + execute(GraphQLSchema(Type), ast, Data(), operation_name="UnknownExample") + assert 'Unknown operation named "UnknownExample".' == str(excinfo.value) + + +def test_raises_if_unknown_operation_name_is_provided(): + doc = 'query Example { a } query OtherExample { a }' + + class Data(object): + a = 'b' + + ast = parse(doc) + Type = GraphQLObjectType('Type', { + 'a': GraphQLField(GraphQLString) + }) + with raises(GraphQLError) as excinfo: + execute(GraphQLSchema(Type), ast, Data()) + assert 'Must provide operation name if query contains multiple operations.' == str(excinfo.value) def test_uses_the_query_schema_for_queries(): @@ -305,7 +362,7 @@ class Data(object): S = GraphQLObjectType('S', { 'a': GraphQLField(GraphQLString) }) - result = execute(GraphQLSchema(Q, M, S), Data(), ast, 'Q') + result = execute(GraphQLSchema(Q, M, S), ast, Data(), operation_name='Q') assert not result.errors assert result.data == {'a': 'b'} @@ -324,7 +381,7 @@ class Data(object): M = GraphQLObjectType('M', { 'c': GraphQLField(GraphQLString) }) - result = execute(GraphQLSchema(Q, M), Data(), ast, 'M') + result = execute(GraphQLSchema(Q, M), ast, Data(), operation_name='M') assert not result.errors assert result.data == {'c': 'd'} @@ -343,7 +400,7 @@ class Data(object): S = GraphQLObjectType('S', { 'a': GraphQLField(GraphQLString) }) - result = execute(GraphQLSchema(Q, subscription=S), Data(), ast, 'S') + result = execute(GraphQLSchema(Q, subscription=S), ast, Data(), operation_name='S') assert not result.errors assert result.data == {'a': 'b'} @@ -368,7 +425,7 @@ class Data(object): Type = GraphQLObjectType('Type', { 'a': GraphQLField(GraphQLString) }) - result = execute(GraphQLSchema(Type), Data(), ast, 'Q') + result = execute(GraphQLSchema(Type), ast, Data(), operation_name='Q') assert not result.errors assert result.data == {'a': 'b'} @@ -382,7 +439,7 @@ def test_does_not_include_illegal_fields_in_output(): M = GraphQLObjectType('M', { 'c': GraphQLField(GraphQLString) }) - result = execute(GraphQLSchema(Q, M), None, ast) + result = execute(GraphQLSchema(Q, M), ast) assert not result.errors assert result.data == {} @@ -406,7 +463,7 @@ def test_does_not_include_arguments_that_were_not_set(): )) ast = parse('{ field(a: true, c: false, e: 0) }') - result = execute(schema, None, ast) + result = execute(schema, ast) assert result.data == { 'field': '{"a":true,"c":false,"e":0}' } @@ -428,7 +485,7 @@ def __init__(self, value): fields={ 'value': GraphQLField(GraphQLString), }, - is_type_of=lambda obj, info: isinstance(obj, Special) + is_type_of=lambda obj, context, info: isinstance(obj, Special) ) schema = GraphQLSchema( @@ -448,7 +505,7 @@ def __init__(self, value): 'specials': [Special('foo'), NotSpecial('bar')] } - result = execute(schema, value, query) + result = execute(schema, query, value) assert result.data == { 'specials': [ @@ -457,7 +514,7 @@ def __init__(self, value): ] } - assert 'Expected value of type "SpecialType" but got NotSpecial.' in str(result.errors) + assert 'Expected value of type "SpecialType" but got: NotSpecial.' in [str(e) for e in result.errors] def test_fails_to_execute_a_query_containing_a_type_definition(): @@ -477,48 +534,30 @@ def test_fails_to_execute_a_query_containing_a_type_definition(): ) with raises(GraphQLError) as excinfo: - execute(schema, None, query) + execute(schema, query) assert excinfo.value.message == 'GraphQL cannot execute a request containing a ObjectTypeDefinition.' -def test_executor_detects_strict_ordering(): - executor = Executor() - assert not executor.enforce_strict_ordering - assert executor.map_type is dict +def test_exceptions_are_reraised_if_specified(mocker): - executor = Executor(map_type=OrderedDict) - assert executor.enforce_strict_ordering - assert executor.map_type is OrderedDict + logger = mocker.patch('graphql.execution.executor.logger') + query = parse(''' + { foo } + ''') -def test_executor_can_enforce_strict_ordering(): - Type = GraphQLObjectType('Type', lambda: { - 'a': GraphQLField(GraphQLString, - resolver=lambda *_: 'Apple'), - 'b': GraphQLField(GraphQLString, - resolver=lambda *_: 'Banana'), - 'c': GraphQLField(GraphQLString, - resolver=lambda *_: 'Cherry'), - 'deep': GraphQLField(Type, resolver=lambda *_: {}), - }) - schema = GraphQLSchema(query=Type) - executor = Executor(execution_middlewares=[SynchronousExecutionMiddleware], map_type=OrderedDict) - query = '{ a b c aa: c cc: c bb: b aaz: a bbz: b deep { b a c deeper: deep { c a b } } ' \ - 'ccz: c zzz: c aaa: a }' - - def check_result(result): - assert not result.errors - - data = result.data - assert isinstance(data, OrderedDict) - assert list(data.keys()) == ['a', 'b', 'c', 'aa', 'cc', 'bb', 'aaz', 'bbz', 'deep', 'ccz', 'zzz', 'aaa'] - deep = data['deep'] - assert isinstance(deep, OrderedDict) - assert list(deep.keys()) == ['b', 'a', 'c', 'deeper'] - deeper = deep['deeper'] - assert isinstance(deeper, OrderedDict) - assert list(deeper.keys()) == ['c', 'a', 'b'] - - check_result(executor.execute(schema, query)) - check_result(executor.execute(schema, query, execute_serially=True)) + def resolver(*_): + raise Exception("UH OH!") + + schema = GraphQLSchema( + GraphQLObjectType( + name='Query', + fields={ + 'foo': GraphQLField(GraphQLString, resolver=resolver) + } + ) + ) + + execute(schema, query) + logger.exception.assert_called_with("An error occurred while resolving field Query.foo") diff --git a/tests/core_execution/test_gevent.py b/graphql/execution/tests/test_executor_gevent.py similarity index 60% rename from tests/core_execution/test_gevent.py rename to graphql/execution/tests/test_executor_gevent.py index 976a54f4..9898729d 100644 --- a/tests/core_execution/test_gevent.py +++ b/graphql/execution/tests/test_executor_gevent.py @@ -1,22 +1,22 @@ # flake8: noqa import gevent -from graphql.core.error import format_error -from graphql.core.execution import Executor -from graphql.core.execution.middlewares.gevent import (GeventExecutionMiddleware, - run_in_greenlet) -from graphql.core.language.location import SourceLocation -from graphql.core.type import (GraphQLField, GraphQLObjectType, GraphQLSchema, - GraphQLString) +from graphql.error import format_error +from graphql.execution import execute +from graphql.language.location import SourceLocation +from graphql.language.parser import parse +from graphql.type import (GraphQLField, GraphQLObjectType, GraphQLSchema, + GraphQLString) + +from ..executors.gevent import GeventExecutor +from .test_mutations import assert_evaluate_mutations_serially def test_gevent_executor(): - @run_in_greenlet def resolver(context, *_): gevent.sleep(0.001) return 'hey' - @run_in_greenlet def resolver_2(context, *_): gevent.sleep(0.003) return 'hey2' @@ -30,22 +30,19 @@ def resolver_3(contest, *_): 'c': GraphQLField(GraphQLString, resolver=resolver_3) }) - doc = '{ a b c }' - executor = Executor([GeventExecutionMiddleware()]) - result = executor.execute(GraphQLSchema(Type), doc) + ast = parse('{ a b c }') + result = execute(GraphQLSchema(Type), ast, executor=GeventExecutor()) assert not result.errors assert result.data == {'a': 'hey', 'b': 'hey2', 'c': 'hey3'} def test_gevent_executor_with_error(): - doc = 'query Example { a, b }' + ast = parse('query Example { a, b }') - @run_in_greenlet def resolver(context, *_): gevent.sleep(0.001) return 'hey' - @run_in_greenlet def resolver_2(context, *_): gevent.sleep(0.003) raise Exception('resolver_2 failed!') @@ -55,8 +52,11 @@ def resolver_2(context, *_): 'b': GraphQLField(GraphQLString, resolver=resolver_2) }) - executor = Executor([GeventExecutionMiddleware()]) - result = executor.execute(GraphQLSchema(Type), doc) + result = execute(GraphQLSchema(Type), ast, executor=GeventExecutor()) formatted_errors = list(map(format_error, result.errors)) assert formatted_errors == [{'locations': [{'line': 1, 'column': 20}], 'message': 'resolver_2 failed!'}] assert result.data == {'a': 'hey', 'b': None} + + +def test_evaluates_mutations_serially(): + assert_evaluate_mutations_serially(executor=GeventExecutor()) diff --git a/graphql/execution/tests/test_executor_thread.py b/graphql/execution/tests/test_executor_thread.py new file mode 100644 index 00000000..062a8e66 --- /dev/null +++ b/graphql/execution/tests/test_executor_thread.py @@ -0,0 +1,222 @@ + +from graphql.error import format_error +from graphql.execution import execute +from graphql.language.parser import parse +from graphql.type import (GraphQLArgument, GraphQLField, GraphQLInt, + GraphQLList, GraphQLObjectType, GraphQLSchema, + GraphQLString) + +from ..executors.thread import ThreadExecutor +from .test_mutations import assert_evaluate_mutations_serially +from .utils import rejected, resolved + + +def test_executes_arbitary_code(): + class Data(object): + a = 'Apple' + b = 'Banana' + c = 'Cookie' + d = 'Donut' + e = 'Egg' + + @property + def f(self): + return resolved('Fish') + + def pic(self, size=50): + return resolved('Pic of size: {}'.format(size)) + + def deep(self): + return DeepData() + + def promise(self): + return resolved(Data()) + + class DeepData(object): + a = 'Already Been Done' + b = 'Boring' + c = ['Contrived', None, resolved('Confusing')] + + def deeper(self): + return [Data(), None, resolved(Data())] + + ast = parse(''' + query Example($size: Int) { + a, + b, + x: c + ...c + f + ...on DataType { + pic(size: $size) + promise { + a + } + } + deep { + a + b + c + deeper { + a + b + } + } + } + fragment c on DataType { + d + e + } + ''') + + expected = { + 'a': 'Apple', + 'b': 'Banana', + 'x': 'Cookie', + 'd': 'Donut', + 'e': 'Egg', + 'f': 'Fish', + 'pic': 'Pic of size: 100', + 'promise': {'a': 'Apple'}, + 'deep': { + 'a': 'Already Been Done', + 'b': 'Boring', + 'c': ['Contrived', None, 'Confusing'], + 'deeper': [ + {'a': 'Apple', 'b': 'Banana'}, + None, + {'a': 'Apple', 'b': 'Banana'}]} + } + + DataType = GraphQLObjectType('DataType', lambda: { + 'a': GraphQLField(GraphQLString), + 'b': GraphQLField(GraphQLString), + 'c': GraphQLField(GraphQLString), + 'd': GraphQLField(GraphQLString), + 'e': GraphQLField(GraphQLString), + 'f': GraphQLField(GraphQLString), + 'pic': GraphQLField( + args={'size': GraphQLArgument(GraphQLInt)}, + type=GraphQLString, + resolver=lambda obj, args, *_: obj.pic(args['size']), + ), + 'deep': GraphQLField(DeepDataType), + 'promise': GraphQLField(DataType), + }) + + DeepDataType = GraphQLObjectType('DeepDataType', { + 'a': GraphQLField(GraphQLString), + 'b': GraphQLField(GraphQLString), + 'c': GraphQLField(GraphQLList(GraphQLString)), + 'deeper': GraphQLField(GraphQLList(DataType)), + }) + + schema = GraphQLSchema(query=DataType) + + def handle_result(result): + assert not result.errors + assert result.data == expected + + handle_result( + execute( + schema, + ast, + Data(), + variable_values={ + 'size': 100}, + operation_name='Example', + executor=ThreadExecutor())) + handle_result(execute(schema, ast, Data(), variable_values={'size': 100}, operation_name='Example')) + + +def test_synchronous_error_nulls_out_error_subtrees(): + ast = parse(''' + { + sync + syncError + syncReturnError + syncReturnErrorList + async + asyncReject + asyncEmptyReject + asyncReturnError + } + ''') + + class Data: + + def sync(self): + return 'sync' + + def syncError(self): + raise Exception('Error getting syncError') + + def syncReturnError(self): + return Exception("Error getting syncReturnError") + + def syncReturnErrorList(self): + return [ + 'sync0', + Exception('Error getting syncReturnErrorList1'), + 'sync2', + Exception('Error getting syncReturnErrorList3') + ] + + def async(self): + return resolved('async') + + def asyncReject(self): + return rejected(Exception('Error getting asyncReject')) + + def asyncEmptyReject(self): + return rejected(Exception('An unknown error occurred.')) + + def asyncReturnError(self): + return resolved(Exception('Error getting asyncReturnError')) + + schema = GraphQLSchema( + query=GraphQLObjectType( + name='Type', + fields={ + 'sync': GraphQLField(GraphQLString), + 'syncError': GraphQLField(GraphQLString), + 'syncReturnError': GraphQLField(GraphQLString), + 'syncReturnErrorList': GraphQLField(GraphQLList(GraphQLString)), + 'async': GraphQLField(GraphQLString), + 'asyncReject': GraphQLField(GraphQLString), + 'asyncEmptyReject': GraphQLField(GraphQLString), + 'asyncReturnError': GraphQLField(GraphQLString), + } + ) + ) + + def sort_key(item): + locations = item['locations'][0] + return (locations['line'], locations['column']) + + def handle_results(result): + assert result.data == { + 'async': 'async', + 'asyncEmptyReject': None, + 'asyncReject': None, + 'asyncReturnError': None, + 'sync': 'sync', + 'syncError': None, + 'syncReturnError': None, + 'syncReturnErrorList': ['sync0', None, 'sync2', None] + } + assert sorted(list(map(format_error, result.errors)), key=sort_key) == sorted([ + {'locations': [{'line': 4, 'column': 9}], 'message': 'Error getting syncError'}, + {'locations': [{'line': 5, 'column': 9}], 'message': 'Error getting syncReturnError'}, + {'locations': [{'line': 6, 'column': 9}], 'message': 'Error getting syncReturnErrorList1'}, + {'locations': [{'line': 6, 'column': 9}], 'message': 'Error getting syncReturnErrorList3'}, + {'locations': [{'line': 8, 'column': 9}], 'message': 'Error getting asyncReject'}, + {'locations': [{'line': 9, 'column': 9}], 'message': 'An unknown error occurred.'}, + {'locations': [{'line': 10, 'column': 9}], 'message': 'Error getting asyncReturnError'} + ], key=sort_key) + + handle_results(execute(schema, ast, Data(), executor=ThreadExecutor())) + + +def test_evaluates_mutations_serially(): + assert_evaluate_mutations_serially(executor=ThreadExecutor()) diff --git a/tests/core_execution/test_lists.py b/graphql/execution/tests/test_lists.py similarity index 66% rename from tests/core_execution/test_lists.py rename to graphql/execution/tests/test_lists.py index cc3f8ab1..034fe7c2 100644 --- a/tests/core_execution/test_lists.py +++ b/graphql/execution/tests/test_lists.py @@ -1,16 +1,15 @@ from collections import namedtuple -from graphql.core.error import format_error -from graphql.core.execution import Executor, execute -from graphql.core.language.parser import parse -from graphql.core.pyutils.defer import fail, succeed -from graphql.core.type import (GraphQLField, GraphQLInt, GraphQLList, - GraphQLNonNull, GraphQLObjectType, - GraphQLSchema) +from graphql.error import format_error +from graphql.execution 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 } }') -executor = Executor() def check(test_data, expected): @@ -27,9 +26,7 @@ def run_check(self): ) schema = GraphQLSchema(query=DataType) - response = executor.execute(schema, ast, data) - assert response.called - response = response.result + response = execute(schema, ast, data) if response.errors: result = { @@ -57,10 +54,10 @@ class Test_ListOfT_Array_T: # [T] Array class Test_ListOfT_Promise_Array_T: # [T] Promise> type = GraphQLList(GraphQLInt) - test_contains_values = check(succeed([1, 2]), {'data': {'nest': {'test': [1, 2]}}}) - test_contains_null = check(succeed([1, None, 2]), {'data': {'nest': {'test': [1, None, 2]}}}) - test_returns_null = check(succeed(None), {'data': {'nest': {'test': None}}}) - test_rejected = check(lambda: fail(Exception('bad')), { + 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'}] }) @@ -69,9 +66,9 @@ class Test_ListOfT_Promise_Array_T: # [T] Promise> class Test_ListOfT_Array_Promise_T: # [T] Array> type = GraphQLList(GraphQLInt) - test_contains_values = check([succeed(1), succeed(2)], {'data': {'nest': {'test': [1, 2]}}}) - test_contains_null = check([succeed(1), succeed(None), succeed(2)], {'data': {'nest': {'test': [1, None, 2]}}}) - test_contains_reject = check(lambda: [succeed(1), fail(Exception('bad')), succeed(2)], { + 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'}] }) @@ -80,9 +77,9 @@ class Test_ListOfT_Array_Promise_T: # [T] Array> class Test_NotNullListOfT_Array_T: # [T]! Array type = GraphQLNonNull(GraphQLList(GraphQLInt)) - test_contains_values = check(succeed([1, 2]), {'data': {'nest': {'test': [1, 2]}}}) - test_contains_null = check(succeed([1, None, 2]), {'data': {'nest': {'test': [1, None, 2]}}}) - test_returns_null = check(succeed(None), { + 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.'}] @@ -92,15 +89,15 @@ class Test_NotNullListOfT_Array_T: # [T]! Array class Test_NotNullListOfT_Promise_Array_T: # [T]! Promise>> type = GraphQLNonNull(GraphQLList(GraphQLInt)) - test_contains_values = check(succeed([1, 2]), {'data': {'nest': {'test': [1, 2]}}}) - test_contains_null = check(succeed([1, None, 2]), {'data': {'nest': {'test': [1, None, 2]}}}) - test_returns_null = check(succeed(None), { + 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: fail(Exception('bad')), { + test_rejected = check(lambda: rejected(Exception('bad')), { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] }) @@ -108,9 +105,9 @@ class Test_NotNullListOfT_Promise_Array_T: # [T]! Promise>> class Test_NotNullListOfT_Array_Promise_T: # [T]! Promise>> type = GraphQLNonNull(GraphQLList(GraphQLInt)) - test_contains_values = check([succeed(1), succeed(2)], {'data': {'nest': {'test': [1, 2]}}}) - test_contains_null = check([succeed(1), succeed(None), succeed(2)], {'data': {'nest': {'test': [1, None, 2]}}}) - test_contains_reject = check(lambda: [succeed(1), fail(Exception('bad')), succeed(2)], { + 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'}] }) @@ -131,16 +128,16 @@ class TestListOfNotNullT_Array_T: # [T!] Array class TestListOfNotNullT_Promise_Array_T: # [T!] Promise> type = GraphQLList(GraphQLNonNull(GraphQLInt)) - test_contains_value = check(succeed([1, 2]), {'data': {'nest': {'test': [1, 2]}}}) - test_contains_null = check(succeed([1, None, 2]), { + 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(succeed(None), {'data': {'nest': {'test': None}}}) + test_returns_null = check(resolved(None), {'data': {'nest': {'test': None}}}) - test_rejected = check(lambda: fail(Exception('bad')), { + test_rejected = check(lambda: rejected(Exception('bad')), { 'data': {'nest': {'test': None}}, 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] }) @@ -149,13 +146,13 @@ class TestListOfNotNullT_Promise_Array_T: # [T!] Promise> class TestListOfNotNullT_Array_Promise_T: # [T!] Array> type = GraphQLList(GraphQLNonNull(GraphQLInt)) - test_contains_values = check([succeed(1), succeed(2)], {'data': {'nest': {'test': [1, 2]}}}) - test_contains_null = check([succeed(1), succeed(None), succeed(2)], { + 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: [succeed(1), fail(Exception('bad')), succeed(2)], { + test_contains_reject = check(lambda: [resolved(1), rejected(Exception('bad')), resolved(2)], { 'data': {'nest': {'test': None}}, 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] }) @@ -180,20 +177,20 @@ class TestNotNullListOfNotNullT_Array_T: # [T!]! Array class TestNotNullListOfNotNullT_Promise_Array_T: # [T!]! Promise> type = GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLInt))) - test_contains_value = check(succeed([1, 2]), {'data': {'nest': {'test': [1, 2]}}}) - test_contains_null = check(succeed([1, None, 2]), { + 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(succeed(None), { + 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: fail(Exception('bad')), { + test_rejected = check(lambda: rejected(Exception('bad')), { 'data': {'nest': None}, 'errors': [{'locations': [{'column': 10, 'line': 1}], 'message': 'bad'}] }) @@ -202,13 +199,13 @@ class TestNotNullListOfNotNullT_Promise_Array_T: # [T!]! Promise> class TestNotNullListOfNotNullT_Array_Promise_T: # [T!]! Array> type = GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLInt))) - test_contains_values = check([succeed(1), succeed(2)], {'data': {'nest': {'test': [1, 2]}}}) - test_contains_null = check([succeed(1), succeed(None), succeed(2)], { + 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: [succeed(1), fail(Exception('bad')), succeed(2)], { + 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/tests/core_execution/test_mutations.py b/graphql/execution/tests/test_mutations.py similarity index 88% rename from tests/core_execution/test_mutations.py rename to graphql/execution/tests/test_mutations.py index df8080e4..3a317fdf 100644 --- a/tests/core_execution/test_mutations.py +++ b/graphql/execution/tests/test_mutations.py @@ -1,8 +1,8 @@ -from graphql.core.execution import execute -from graphql.core.language.parser import parse -from graphql.core.type import (GraphQLArgument, GraphQLField, GraphQLInt, - GraphQLList, GraphQLObjectType, GraphQLSchema, - GraphQLString) +from graphql.execution import execute +from graphql.language.parser import parse +from graphql.type import (GraphQLArgument, GraphQLField, GraphQLInt, + GraphQLList, GraphQLObjectType, GraphQLSchema, + GraphQLString) class NumberHolder(object): @@ -66,7 +66,7 @@ def promise_and_fail_to_change_the_number(self, n): schema = GraphQLSchema(QueryType, MutationType) -def test_evaluates_mutations_serially(): +def assert_evaluate_mutations_serially(executor=None): doc = '''mutation M { first: immediatelyChangeTheNumber(newNumber: 1) { theNumber @@ -85,7 +85,7 @@ def test_evaluates_mutations_serially(): } }''' ast = parse(doc) - result = execute(schema, Root(6), ast, 'M') + result = execute(schema, ast, Root(6), operation_name='M', executor=executor) assert not result.errors assert result.data == \ { @@ -97,6 +97,10 @@ def test_evaluates_mutations_serially(): } +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) { @@ -119,7 +123,7 @@ def test_evaluates_mutations_correctly_in_the_presense_of_a_failed_mutation(): } }''' ast = parse(doc) - result = execute(schema, Root(6), ast, 'M') + result = execute(schema, ast, Root(6), operation_name='M') assert result.data == \ { 'first': {'theNumber': 1}, diff --git a/tests/core_execution/test_nonnull.py b/graphql/execution/tests/test_nonnull.py similarity index 94% rename from tests/core_execution/test_nonnull.py rename to graphql/execution/tests/test_nonnull.py index 2fc4b281..bc48de3f 100644 --- a/tests/core_execution/test_nonnull.py +++ b/graphql/execution/tests/test_nonnull.py @@ -1,19 +1,17 @@ -from collections import OrderedDict -from graphql.core.error import format_error -from graphql.core.execution import Executor, execute -from graphql.core.language.parser import parse -from graphql.core.pyutils.defer import fail, succeed -from graphql.core.type import (GraphQLField, GraphQLNonNull, GraphQLObjectType, - GraphQLSchema, GraphQLString) +from graphql.error import format_error +from graphql.execution 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') -executor = Executor(map_type=OrderedDict) - class ThrowingData(object): @@ -24,10 +22,10 @@ def nonNullSync(self): raise non_null_sync_error def promise(self): - return fail(promise_error) + return rejected(promise_error) def nonNullPromise(self): - return fail(non_null_promise_error) + return rejected(non_null_promise_error) def nest(self): return ThrowingData() @@ -36,10 +34,10 @@ def nonNullNest(self): return ThrowingData() def promiseNest(self): - return succeed(ThrowingData()) + return resolved(ThrowingData()) def nonNullPromiseNest(self): - return succeed(ThrowingData()) + return resolved(ThrowingData()) class NullingData(object): @@ -51,7 +49,10 @@ def nonNullSync(self): return None def promise(self): - return succeed(None) + return resolved(None) + + def nonNullPromise(self): + return resolved(None) def nest(self): return NullingData() @@ -60,10 +61,10 @@ def nonNullNest(self): return NullingData() def promiseNest(self): - return succeed(NullingData()) + return resolved(NullingData()) def nonNullPromiseNest(self): - return succeed(NullingData()) + return resolved(NullingData()) DataType = GraphQLObjectType('DataType', lambda: { @@ -82,9 +83,7 @@ def nonNullPromiseNest(self): def check(doc, data, expected): ast = parse(doc) - response = executor.execute(schema, ast, data) - assert response.called - response = response.result + response = execute(schema, ast, data) if response.errors: result = { diff --git a/graphql/execution/tests/test_resolve.py b/graphql/execution/tests/test_resolve.py new file mode 100644 index 00000000..9b079551 --- /dev/null +++ b/graphql/execution/tests/test_resolve.py @@ -0,0 +1,71 @@ +import json +from collections import OrderedDict + +from graphql import graphql +from graphql.type import (GraphQLArgument, GraphQLField, GraphQLInt, + GraphQLObjectType, GraphQLSchema, GraphQLString) + + +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!"}]'} + ] diff --git a/tests/core_execution/test_union_interface.py b/graphql/execution/tests/test_union_interface.py similarity index 82% rename from tests/core_execution/test_union_interface.py rename to graphql/execution/tests/test_union_interface.py index e8492c13..27eeedd8 100644 --- a/tests/core_execution/test_union_interface.py +++ b/graphql/execution/tests/test_union_interface.py @@ -1,9 +1,8 @@ -from graphql.core.execution import execute -from graphql.core.language.parser import parse -from graphql.core.type import (GraphQLBoolean, GraphQLField, - GraphQLInterfaceType, GraphQLList, - GraphQLObjectType, GraphQLSchema, GraphQLString, - GraphQLUnionType) +from graphql.execution import execute +from graphql.language.parser import parse +from graphql.type import (GraphQLBoolean, GraphQLField, GraphQLInterfaceType, + GraphQLList, GraphQLObjectType, GraphQLSchema, + GraphQLString, GraphQLUnionType) class Dog(object): @@ -39,7 +38,7 @@ def __init__(self, name, pets, friends): 'name': GraphQLField(GraphQLString), 'barks': GraphQLField(GraphQLBoolean), }, - is_type_of=lambda value, info: isinstance(value, Dog) + is_type_of=lambda value, context, info: isinstance(value, Dog) ) CatType = GraphQLObjectType( @@ -49,11 +48,11 @@ def __init__(self, name, pets, friends): 'name': GraphQLField(GraphQLString), 'meows': GraphQLField(GraphQLBoolean), }, - is_type_of=lambda value, info: isinstance(value, Cat) + is_type_of=lambda value, context, info: isinstance(value, Cat) ) -def resolve_pet_type(value, info): +def resolve_pet_type(value, context, info): if isinstance(value, Dog): return DogType if isinstance(value, Cat): @@ -71,10 +70,10 @@ def resolve_pet_type(value, info): 'pets': GraphQLField(GraphQLList(PetType)), 'friends': GraphQLField(GraphQLList(NamedType)), }, - is_type_of=lambda value, info: isinstance(value, Person) + is_type_of=lambda value, context, info: isinstance(value, Person) ) -schema = GraphQLSchema(PersonType) +schema = GraphQLSchema(query=PersonType, types=[PetType]) garfield = Cat('Garfield', False) odie = Dog('Odie', True) @@ -107,7 +106,8 @@ def test_can_introspect_on_union_and_intersection_types(): } }''') - result = execute(schema, None, ast) + result = execute(schema, ast) + assert not result.errors assert result.data == { 'Named': { 'enumValues': None, @@ -115,7 +115,7 @@ def test_can_introspect_on_union_and_intersection_types(): 'kind': 'INTERFACE', 'interfaces': None, 'fields': [{'name': 'name'}], - 'possibleTypes': [{'name': 'Dog'}, {'name': 'Cat'}, {'name': 'Person'}], + 'possibleTypes': [{'name': 'Person'}, {'name': 'Dog'}, {'name': 'Cat'}], 'inputFields': None }, 'Pet': { @@ -144,7 +144,7 @@ def test_executes_using_union_types(): } } ''') - result = execute(schema, john, ast) + result = execute(schema, ast, john) assert not result.errors assert result.data == { '__typename': 'Person', @@ -175,7 +175,7 @@ def test_executes_union_types_with_inline_fragment(): } } ''') - result = execute(schema, john, ast) + result = execute(schema, ast, john) assert not result.errors assert result.data == { '__typename': 'Person', @@ -201,7 +201,7 @@ def test_executes_using_interface_types(): } } ''') - result = execute(schema, john, ast) + result = execute(schema, ast, john) assert not result.errors assert result.data == { '__typename': 'Person', @@ -231,7 +231,7 @@ def test_executes_interface_types_with_inline_fragment(): } } ''') - result = execute(schema, john, ast) + result = execute(schema, ast, john) assert not result.errors assert result.data == { '__typename': 'Person', @@ -273,7 +273,7 @@ def test_allows_fragment_conditions_to_be_abstract_types(): } } ''') - result = execute(schema, john, ast) + result = execute(schema, ast, john) assert not result.errors assert result.data == { '__typename': 'Person', @@ -301,7 +301,7 @@ def test_only_include_fields_from_matching_fragment_condition(): } } ''') - result = execute(schema, john, ast) + result = execute(schema, ast, john) assert not result.errors assert result.data == { 'pets': [ @@ -312,12 +312,15 @@ def test_only_include_fields_from_matching_fragment_condition(): def test_gets_execution_info_in_resolver(): - encountered_schema = [None] - encountered_root_value = [None] - - def resolve_type(obj, info): - encountered_schema[0] = info.schema - encountered_root_value[0] = info.root_value + 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( @@ -339,12 +342,15 @@ def resolve_type(obj, info): schema2 = GraphQLSchema(query=PersonType2) john2 = Person('John', [], [liz]) + context = {'hey'} ast = parse('''{ name, friends { name } }''') - result = execute(schema2, john2, ast) + result = execute(schema2, ast, john2, context_value=context) + assert not result.errors assert result.data == { 'name': 'John', 'friends': [{'name': 'Liz'}] } - assert encountered_schema[0] == schema2 - assert encountered_root_value[0] == john2 + assert encountered.schema == schema2 + assert encountered.root_value == john2 + assert encountered.context == context diff --git a/tests/core_execution/test_variables.py b/graphql/execution/tests/test_variables.py similarity index 95% rename from tests/core_execution/test_variables.py rename to graphql/execution/tests/test_variables.py index 72745a36..efabda89 100644 --- a/tests/core_execution/test_variables.py +++ b/graphql/execution/tests/test_variables.py @@ -2,13 +2,13 @@ from pytest import raises -from graphql.core.error import GraphQLError, format_error -from graphql.core.execution import execute -from graphql.core.language.parser import parse -from graphql.core.type import (GraphQLArgument, GraphQLField, - GraphQLInputObjectField, GraphQLInputObjectType, - GraphQLList, GraphQLNonNull, GraphQLObjectType, - GraphQLScalarType, GraphQLSchema, GraphQLString) +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', @@ -27,7 +27,7 @@ stringify = lambda obj: json.dumps(obj, sort_keys=True) -def input_to_json(obj, args, info): +def input_to_json(obj, args, context, info): input = args.get('input') if input: return stringify(input) @@ -91,7 +91,7 @@ def input_to_json(obj, args, info): def check(doc, expected, args=None): ast = parse(doc) - response = execute(schema, None, ast, args=args) + response = execute(schema, ast, variable_values=args) if response.errors: result = { @@ -141,6 +141,19 @@ def test_does_not_use_incorrect_value(): }) +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 = ''' diff --git a/graphql/execution/tests/utils.py b/graphql/execution/tests/utils.py new file mode 100644 index 00000000..0c260810 --- /dev/null +++ b/graphql/execution/tests/utils.py @@ -0,0 +1,9 @@ +from promise import Promise + + +def resolved(value): + return Promise.fulfilled(value) + + +def rejected(error): + return Promise.rejected(error) diff --git a/graphql/core/execution/values.py b/graphql/execution/values.py similarity index 95% rename from graphql/core/execution/values.py rename to graphql/execution/values.py index c31f2145..cf1bff1e 100644 --- a/graphql/core/execution/values.py +++ b/graphql/execution/values.py @@ -32,9 +32,11 @@ def get_variable_values(schema, definition_asts, inputs): def get_argument_values(arg_defs, arg_asts, variables): """Prepares an object map of argument values given a list of argument definitions and list of argument AST nodes.""" + if not arg_defs: + return {} + if arg_asts: arg_ast_map = {arg.name.value: arg for arg in arg_asts} - else: arg_ast_map = {} @@ -75,14 +77,15 @@ def get_variable_value(schema, definition_ast, input): [definition_ast] ) - errors = is_valid_value(input, type) + input_type = type + errors = is_valid_value(input, input_type) if not errors: if input is None: default_value = definition_ast.default_value if default_value: - return value_from_ast(default_value, type) + return value_from_ast(default_value, input_type) - return coerce_value(type, input) + return coerce_value(input_type, input) if input is None: raise GraphQLError( diff --git a/graphql/graphql.py b/graphql/graphql.py new file mode 100644 index 00000000..e156f703 --- /dev/null +++ b/graphql/graphql.py @@ -0,0 +1,53 @@ +from .execution import ExecutionResult, execute +from .language.parser import parse +from .language.source import Source +from .validation import validate + + +# This is the primary entry point function for fulfilling GraphQL operations +# by parsing, validating, and executing a GraphQL document along side a +# GraphQL schema. + +# More sophisticated GraphQL servers, such as those which persist queries, +# may wish to separate the validation and execution phases to a static time +# tooling step, and a server runtime step. + +# schema: +# The GraphQL type system to use when validating and executing a query. +# requestString: +# A GraphQL language formatted string representing the requested operation. +# rootValue: +# The value provided as the first argument to resolver functions on the top +# level type (e.g. the query object type). +# variableValues: +# A mapping of variable name to runtime value to use for all variables +# defined in the requestString. +# operationName: +# The name of the operation to use if requestString contains multiple +# possible operations. Can be omitted if requestString contains only +# one operation. +def graphql(schema, request_string='', root_value=None, context_value=None, + variable_values=None, operation_name=None, executor=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 + ) + except Exception as e: + return ExecutionResult( + errors=[e], + invalid=True, + ) diff --git a/graphql/core/utils/__init__.py b/graphql/language/__init__.py similarity index 100% rename from graphql/core/utils/__init__.py rename to graphql/language/__init__.py diff --git a/graphql/core/language/ast.py b/graphql/language/ast.py similarity index 91% rename from graphql/core/language/ast.py rename to graphql/language/ast.py index f4961cdc..4b053784 100644 --- a/graphql/core/language/ast.py +++ b/graphql/language/ast.py @@ -834,8 +834,80 @@ def __hash__(self): return id(self) +# Type System Definition + class TypeDefinition(Node): - __slots__ = () + pass + + +class TypeSystemDefinition(TypeDefinition): + pass + + +class SchemaDefinition(TypeSystemDefinition): + __slots__ = ('loc', 'operation_types',) + _fields = ('operation_types',) + + def __init__(self, operation_types, loc=None): + self.operation_types = operation_types + self.loc = loc + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, SchemaDefinition) and + self.operation_types == other.operation_types + ) + ) + + def __repr__(self): + return ('SchemaDefinition(' + 'operation_types={self.operation_types!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.operation_types, + self.loc + ) + + def __hash__(self): + return id(self) + + +class OperationTypeDefinition(Node): + __slots__ = ('loc', 'operation', 'type',) + _fields = ('operation', 'type',) + + def __init__(self, operation, type, loc=None): + self.operation = operation + self.type = type + self.loc = loc + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, OperationTypeDefinition) and + self.operation == other.operation and + self.type == other.type + ) + ) + + def __repr__(self): + return ('OperationTypeDefinition(' + 'operation={self.operation!r}' + ', type={self.type!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.operation, + self.type, + self.loc + ) + + def __hash__(self): + return id(self) class ObjectTypeDefinition(TypeDefinition): @@ -1166,7 +1238,7 @@ def __hash__(self): return id(self) -class TypeExtensionDefinition(TypeDefinition): +class TypeExtensionDefinition(TypeSystemDefinition): __slots__ = ('loc', 'definition',) _fields = ('definition',) @@ -1196,3 +1268,42 @@ def __copy__(self): def __hash__(self): return id(self) + + +class DirectiveDefinition(TypeSystemDefinition): + __slots__ = ('loc', 'name', 'arguments', 'locations') + _fields = ('name', 'locations') + + def __init__(self, name, locations, arguments=None, loc=None): + self.name = name + self.locations = locations + self.loc = loc + self.arguments = arguments + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, DirectiveDefinition) and + self.name == other.name and + self.locations == other.locations and + self.loc == other.loc and + self.arguments == other.arguments + ) + ) + + def __repr__(self): + return ('DirectiveDefinition(' + 'name={self.name!r}, ' + 'locations={self.locations!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.name, + self.locations, + self.arguments, + self.loc, + ) + + def __hash__(self): + return id(self) diff --git a/graphql/language/base.py b/graphql/language/base.py new file mode 100644 index 00000000..f6d9d91b --- /dev/null +++ b/graphql/language/base.py @@ -0,0 +1,19 @@ +from .lexer import Lexer +from .location import get_location +from .parser import parse, parse_value +from .printer import print_ast +from .source import Source +from .visitor import BREAK, ParallelVisitor, TypeInfoVisitor, visit + +__all__ = [ + 'Lexer', + 'get_location', + 'parse', + 'parse_value', + 'print_ast', + 'Source', + 'BREAK', + 'ParallelVisitor', + 'TypeInfoVisitor', + 'visit', +] diff --git a/graphql/core/language/lexer.py b/graphql/language/lexer.py similarity index 96% rename from graphql/core/language/lexer.py rename to graphql/language/lexer.py index 9c5066c6..4eae3c2a 100644 --- a/graphql/core/language/lexer.py +++ b/graphql/language/lexer.py @@ -2,7 +2,7 @@ from six import unichr -from .error import LanguageError +from ..error import GraphQLSyntaxError __all__ = ['Token', 'Lexer', 'TokenKind', 'get_token_desc', 'get_token_kind_desc'] @@ -156,7 +156,7 @@ def read_token(source, from_position): code = char_code_at(body, position) if code < 0x0020 and code not in (0x0009, 0x000A, 0x000D): - raise LanguageError( + raise GraphQLSyntaxError( source, position, u'Invalid character {}.'.format(print_char_code(code)) ) @@ -179,7 +179,7 @@ def read_token(source, from_position): elif code == 34: # " return read_string(source, position) - raise LanguageError( + raise GraphQLSyntaxError( source, position, u'Unexpected character {}.'.format(print_char_code(code))) @@ -241,7 +241,7 @@ def read_number(source, start, first_code): code = char_code_at(body, position) if code is not None and 48 <= code <= 57: - raise LanguageError( + raise GraphQLSyntaxError( source, position, u'Invalid number, unexpected digit after 0: {}.'.format(print_char_code(code)) @@ -291,7 +291,7 @@ def read_digits(source, start, first_code): return position - raise LanguageError( + raise GraphQLSyntaxError( source, position, u'Invalid number, expected digit but got: {}.'.format(print_char_code(code)) @@ -338,7 +338,7 @@ def read_string(source, start): break if code < 0x0020 and code != 0x0009: - raise LanguageError( + raise GraphQLSyntaxError( source, position, u'Invalid character within String: {}.'.format(print_char_code(code)) @@ -362,7 +362,7 @@ def read_string(source, start): ) if char_code < 0: - raise LanguageError( + raise GraphQLSyntaxError( source, position, u'Invalid character escape sequence: \\u{}.'.format(body[position + 1: position + 5]) ) @@ -370,7 +370,7 @@ def read_string(source, start): append(unichr(char_code)) position += 4 else: - raise LanguageError( + raise GraphQLSyntaxError( source, position, u'Invalid character escape sequence: \\{}.'.format(unichr(code)) ) @@ -379,7 +379,7 @@ def read_string(source, start): chunk_start = position if code != 34: # Quote (") - raise LanguageError(source, position, 'Unterminated string') + raise GraphQLSyntaxError(source, position, 'Unterminated string') append(body[chunk_start:position]) return Token(TokenKind.STRING, start, position + 1, u''.join(value)) diff --git a/graphql/core/language/location.py b/graphql/language/location.py similarity index 100% rename from graphql/core/language/location.py rename to graphql/language/location.py diff --git a/graphql/core/language/parser.py b/graphql/language/parser.py similarity index 88% rename from graphql/core/language/parser.py rename to graphql/language/parser.py index ecd55ff1..87a29b5b 100644 --- a/graphql/core/language/parser.py +++ b/graphql/language/parser.py @@ -1,7 +1,7 @@ from six import string_types from . import ast -from .error import LanguageError +from ..error import GraphQLSyntaxError from .lexer import Lexer, TokenKind, get_token_desc, get_token_kind_desc from .source import Source @@ -109,7 +109,7 @@ def expect(parser, kind): advance(parser) return token - raise LanguageError( + raise GraphQLSyntaxError( parser.source, token.start, u'Expected {}, found {}'.format( @@ -128,7 +128,7 @@ def expect_keyword(parser, value): advance(parser) return token - raise LanguageError( + raise GraphQLSyntaxError( parser.source, token.start, u'Expected "{}", found {}'.format(value, get_token_desc(token)) @@ -139,7 +139,7 @@ def unexpected(parser, at_token=None): """Helper function for creating an error when an unexpected lexed token is encountered.""" token = at_token or parser.token - return LanguageError( + return GraphQLSyntaxError( parser.source, token.start, u'Unexpected {}'.format(get_token_desc(token)) @@ -209,10 +209,8 @@ def parse_definition(parser): return parse_operation_definition(parser) elif name == 'fragment': return parse_fragment_definition(parser) - elif name in ('type', 'interface', 'union', 'scalar', 'enum', 'input'): - return parse_type_definition(parser) - elif name == 'extend': - return parse_type_extension_definition(parser) + elif name in ('schema', 'scalar', 'type', 'interface', 'union', 'enum', 'input', 'extend', 'directive'): + return parse_type_system_definition(parser) raise unexpected(parser) @@ -230,8 +228,7 @@ def parse_operation_definition(parser): loc=loc(parser, start) ) - operation_token = expect(parser, TokenKind.NAME) - operation = operation_token.value + operation = parse_operation_type(parser) name = None if peek(parser, TokenKind.NAME): @@ -247,6 +244,19 @@ def parse_operation_definition(parser): ) +def parse_operation_type(parser): + operation_token = expect(parser, TokenKind.NAME) + operation = operation_token.value + if operation == 'query': + return 'query' + elif operation == 'mutation': + return 'mutation' + elif operation == 'subscription': + return 'subscription' + + raise unexpected(parser, operation_token) + + def parse_variable_definitions(parser): if peek(parser, TokenKind.PAREN_L): return many( @@ -508,13 +518,19 @@ def parse_named_type(parser): ) -def parse_type_definition(parser): +def parse_type_system_definition(parser): if not peek(parser, TokenKind.NAME): raise unexpected(parser) name = parser.token.value - if name == 'type': + if name == 'schema': + return parse_schema_definition(parser) + + elif name == 'scalar': + return parse_scalar_type_definition(parser) + + elif name == 'type': return parse_object_type_definition(parser) elif name == 'interface': @@ -523,18 +539,59 @@ def parse_type_definition(parser): elif name == 'union': return parse_union_type_definition(parser) - elif name == 'scalar': - return parse_scalar_type_definition(parser) - elif name == 'enum': return parse_enum_type_definition(parser) elif name == 'input': return parse_input_object_type_definition(parser) + elif name == 'extend': + return parse_type_extension_definition(parser) + + elif name == 'directive': + return parse_directive_definition(parser) + raise unexpected(parser) +def parse_schema_definition(parser): + start = parser.token.start + expect_keyword(parser, 'schema') + operation_types = many( + parser, + TokenKind.BRACE_L, + parse_operation_type_definition, + TokenKind.BRACE_R + ) + + return ast.SchemaDefinition( + operation_types=operation_types, + loc=loc(parser, start) + ) + + +def parse_operation_type_definition(parser): + start = parser.token.start + operation = parse_operation_type(parser) + expect(parser, TokenKind.COLON) + + return ast.OperationTypeDefinition( + operation=operation, + type=parse_named_type(parser), + loc=loc(parser, start) + ) + + +def parse_scalar_type_definition(parser): + start = parser.token.start + expect_keyword(parser, 'scalar') + + return ast.ScalarTypeDefinition( + name=parse_name(parser), + loc=loc(parser, start) + ) + + def parse_object_type_definition(parser): start = parser.token.start expect_keyword(parser, 'type') @@ -628,16 +685,6 @@ def parse_union_members(parser): return members -def parse_scalar_type_definition(parser): - start = parser.token.start - expect_keyword(parser, 'scalar') - - return ast.ScalarTypeDefinition( - name=parse_name(parser), - loc=loc(parser, start) - ) - - def parse_enum_type_definition(parser): start = parser.token.start expect_keyword(parser, 'enum') @@ -677,3 +724,33 @@ def parse_type_extension_definition(parser): definition=parse_object_type_definition(parser), loc=loc(parser, start) ) + + +def parse_directive_definition(parser): + start = parser.token.start + expect_keyword(parser, 'directive') + expect(parser, TokenKind.AT) + + name = parse_name(parser) + args = parse_argument_defs(parser) + expect_keyword(parser, 'on') + + locations = parse_directive_locations(parser) + return ast.DirectiveDefinition( + name=name, + locations=locations, + arguments=args, + loc=loc(parser, start) + ) + + +def parse_directive_locations(parser): + locations = [] + + while True: + locations.append(parse_name(parser)) + + if not skip(parser, TokenKind.PIPE): + break + + return locations diff --git a/graphql/core/language/printer.py b/graphql/language/printer.py similarity index 92% rename from graphql/core/language/printer.py rename to graphql/language/printer.py index 9ab7f736..2f7e1985 100644 --- a/graphql/core/language/printer.py +++ b/graphql/language/printer.py @@ -111,6 +111,15 @@ def leave_NonNullType(self, node, *args): # Type Definitions: + def leave_SchemaDefinition(self, node, *args): + return 'schema ' + block(node.operation_types) + + def leave_OperationTypeDefinition(self, node, *args): + return '{}: {}'.format(node.operation, node.type) + + def leave_ScalarTypeDefinition(self, node, *args): + return 'scalar ' + node.name + def leave_ObjectTypeDefinition(self, node, *args): return ( 'type ' + node.name + ' ' + @@ -130,9 +139,6 @@ def leave_InterfaceTypeDefinition(self, node, *args): def leave_UnionTypeDefinition(self, node, *args): return 'union ' + node.name + ' = ' + join(node.types, ' | ') - def leave_ScalarTypeDefinition(self, node, *args): - return 'scalar ' + node.name - def leave_EnumTypeDefinition(self, node, *args): return 'enum ' + node.name + ' ' + block(node.values) @@ -145,6 +151,10 @@ def leave_InputObjectTypeDefinition(self, node, *args): def leave_TypeExtensionDefinition(self, node, *args): return 'extend ' + node.definition + def leave_DirectiveDefinition(self, node, *args): + return 'directive @{}{} on {}'.format(node.name, wrap( + '(', join(node.arguments, ', '), ')'), ' | '.join(node.locations)) + def join(maybe_list, separator=''): if maybe_list: diff --git a/graphql/core/language/source.py b/graphql/language/source.py similarity index 100% rename from graphql/core/language/source.py rename to graphql/language/source.py diff --git a/tests/core_execution/__init__.py b/graphql/language/tests/__init__.py similarity index 100% rename from tests/core_execution/__init__.py rename to graphql/language/tests/__init__.py diff --git a/tests/core_language/fixtures.py b/graphql/language/tests/fixtures.py similarity index 90% rename from tests/core_language/fixtures.py rename to graphql/language/tests/fixtures.py index 25c93fce..774a8311 100644 --- a/tests/core_language/fixtures.py +++ b/graphql/language/tests/fixtures.py @@ -67,6 +67,11 @@ # LICENSE file in the root directory of this source tree. An additional grant # of patent rights can be found in the PATENTS file in the same directory. +schema { + query: QueryType + mutation: MutationType +} + type Foo implements Bar { one: Type two(argument: InputType!): Type @@ -98,4 +103,8 @@ extend type Foo { seven(argument: [String]): Type } + +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT """ diff --git a/tests/core_language/test_ast.py b/graphql/language/tests/test_ast.py similarity index 87% rename from tests/core_language/test_ast.py rename to graphql/language/tests/test_ast.py index 67fd0ac7..269d1f09 100644 --- a/tests/core_language/test_ast.py +++ b/graphql/language/tests/test_ast.py @@ -1,6 +1,6 @@ import copy -from graphql.core.language.visitor_meta import QUERY_DOCUMENT_KEYS, VisitorMeta +from graphql.language.visitor_meta import QUERY_DOCUMENT_KEYS, VisitorMeta def test_ast_is_hashable(): diff --git a/tests/core_language/test_lexer.py b/graphql/language/tests/test_lexer.py similarity index 83% rename from tests/core_language/test_lexer.py rename to graphql/language/tests/test_lexer.py index 37075f6e..44283355 100644 --- a/tests/core_language/test_lexer.py +++ b/graphql/language/tests/test_lexer.py @@ -1,8 +1,8 @@ from pytest import raises -from graphql.core.language.error import LanguageError -from graphql.core.language.lexer import Lexer, Token, TokenKind -from graphql.core.language.source import Source +from graphql.error import GraphQLSyntaxError +from graphql.language.lexer import Lexer, Token, TokenKind +from graphql.language.source import Source def lex_one(s): @@ -15,7 +15,7 @@ def test_repr_token(): def test_disallows_uncommon_control_characters(): - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'\u0007') assert u'Syntax Error GraphQL (1:1) Invalid character "\\u0007"' in excinfo.value.message @@ -42,7 +42,7 @@ def test_skips_whitespace(): def test_errors_respect_whitespace(): - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u""" ? @@ -70,55 +70,55 @@ def test_lexes_strings(): def test_lex_reports_useful_string_errors(): - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"') assert u'Syntax Error GraphQL (1:2) Unterminated string' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"no end quote') assert u'Syntax Error GraphQL (1:14) Unterminated string' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"contains unescaped \u0007 control char"') assert u'Syntax Error GraphQL (1:21) Invalid character within String: "\\u0007".' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"null-byte is not \u0000 end of file"') assert u'Syntax Error GraphQL (1:19) Invalid character within String: "\\u0000".' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"multi\nline"') assert u'Syntax Error GraphQL (1:7) Unterminated string' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"multi\rline"') assert u'Syntax Error GraphQL (1:7) Unterminated string' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"bad \\z esc"') assert u'Syntax Error GraphQL (1:7) Invalid character escape sequence: \\z.' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"bad \\x esc"') assert u'Syntax Error GraphQL (1:7) Invalid character escape sequence: \\x.' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"bad \\u1 esc"') assert u'Syntax Error GraphQL (1:7) Invalid character escape sequence: \\u1 es.' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"bad \\u0XX1 esc"') assert u'Syntax Error GraphQL (1:7) Invalid character escape sequence: \\u0XX1.' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"bad \\uXXXX esc"') assert u'Syntax Error GraphQL (1:7) Invalid character escape sequence: \\uXXXX' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"bad \\uFXXX esc"') assert u'Syntax Error GraphQL (1:7) Invalid character escape sequence: \\uFXXX.' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'"bad \\uXXXF esc"') assert u'Syntax Error GraphQL (1:7) Invalid character escape sequence: \\uXXXF.' in excinfo.value.message @@ -143,35 +143,35 @@ def test_lexes_numbers(): def test_lex_reports_useful_number_errors(): - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'00') assert u'Syntax Error GraphQL (1:2) Invalid number, unexpected digit after 0: "0".' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'+1') assert u'Syntax Error GraphQL (1:1) Unexpected character "+"' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'1.') assert u'Syntax Error GraphQL (1:3) Invalid number, expected digit but got: .' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'.123') assert u'Syntax Error GraphQL (1:1) Unexpected character ".".' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'1.A') assert u'Syntax Error GraphQL (1:3) Invalid number, expected digit but got: "A".' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'-A') assert u'Syntax Error GraphQL (1:2) Invalid number, expected digit but got: "A".' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'1.0e') assert u'Syntax Error GraphQL (1:5) Invalid number, expected digit but got: .' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'1.0eA') assert u'Syntax Error GraphQL (1:5) Invalid number, expected digit but got: "A".' in excinfo.value.message @@ -193,19 +193,19 @@ def test_lexes_punctuation(): def test_lex_reports_useful_unknown_character_error(): - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'..') assert u'Syntax Error GraphQL (1:1) Unexpected character "."' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'?') assert u'Syntax Error GraphQL (1:1) Unexpected character "?"' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'\u203B') assert u'Syntax Error GraphQL (1:1) Unexpected character "\\u203B"' in excinfo.value.message - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lex_one(u'\u200b') assert u'Syntax Error GraphQL (1:1) Unexpected character "\\u200B"' in excinfo.value.message @@ -215,7 +215,7 @@ def test_lex_reports_useful_information_for_dashes_in_names(): lexer = Lexer(Source(q)) first_token = lexer.next_token() assert first_token == Token(TokenKind.NAME, 0, 1, 'a') - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: lexer.next_token() assert u'Syntax Error GraphQL (1:3) Invalid number, expected digit but got: "b".' in excinfo.value.message diff --git a/tests/core_language/test_location.py b/graphql/language/tests/test_location.py similarity index 68% rename from tests/core_language/test_location.py rename to graphql/language/tests/test_location.py index 84713579..c18fe42e 100644 --- a/tests/core_language/test_location.py +++ b/graphql/language/tests/test_location.py @@ -1,4 +1,4 @@ -from graphql.core.language.location import SourceLocation +from graphql.language.location import SourceLocation def test_repr_source_location(): diff --git a/tests/core_language/test_parser.py b/graphql/language/tests/test_parser.py similarity index 90% rename from tests/core_language/test_parser.py rename to graphql/language/tests/test_parser.py index 6a41e6fd..3519cef5 100644 --- a/tests/core_language/test_parser.py +++ b/graphql/language/tests/test_parser.py @@ -1,10 +1,10 @@ from pytest import raises -from graphql.core.language import ast -from graphql.core.language.error import LanguageError -from graphql.core.language.location import SourceLocation -from graphql.core.language.parser import Loc, parse -from graphql.core.language.source import Source +from graphql.error import GraphQLSyntaxError +from graphql.language import ast +from graphql.language.location import SourceLocation +from graphql.language.parser import Loc, parse +from graphql.language.source import Source from .fixtures import KITCHEN_SINK @@ -15,7 +15,7 @@ def test_repr_loc(): def test_parse_provides_useful_errors(): - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: parse("""{""") assert ( u'Syntax Error GraphQL (1:2) Expected Name, found EOF\n' @@ -28,27 +28,27 @@ def test_parse_provides_useful_errors(): assert excinfo.value.positions == [1] assert excinfo.value.locations == [SourceLocation(line=1, column=2)] - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: parse("""{ ...MissingOn } fragment MissingOn Type """) assert 'Syntax Error GraphQL (2:20) Expected "on", found Name "Type"' in str(excinfo.value) - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: parse('{ field: {} }') assert 'Syntax Error GraphQL (1:10) Expected Name, found {' in str(excinfo.value) - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: parse('notanoperation Foo { field }') assert 'Syntax Error GraphQL (1:1) Unexpected Name "notanoperation"' in str(excinfo.value) - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: parse('...') assert 'Syntax Error GraphQL (1:1) Unexpected ...' in str(excinfo.value) def test_parse_provides_useful_error_when_using_source(): - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: parse(Source('query', 'MyQuery.graphql')) assert 'Syntax Error MyQuery.graphql (1:6) Expected {, found EOF' in str(excinfo.value) @@ -58,27 +58,27 @@ def test_parses_variable_inline_values(): def test_parses_constant_default_values(): - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: parse('query Foo($x: Complex = { a: { b: [ $var ] } }) { field }') assert 'Syntax Error GraphQL (1:37) Unexpected $' in str(excinfo.value) def test_does_not_accept_fragments_named_on(): - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: parse('fragment on on on { on }') assert 'Syntax Error GraphQL (1:10) Unexpected Name "on"' in excinfo.value.message def test_does_not_accept_fragments_spread_of_on(): - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: parse('{ ...on }') assert 'Syntax Error GraphQL (1:9) Expected Name, found }' in excinfo.value.message def test_does_not_allow_null_value(): - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: parse('{ fieldWithNullableStringInput(input: null) }') assert 'Syntax Error GraphQL (1:39) Unexpected Name "null"' in excinfo.value.message diff --git a/tests/core_language/test_printer.py b/graphql/language/tests/test_printer.py similarity index 89% rename from tests/core_language/test_printer.py rename to graphql/language/tests/test_printer.py index b4cf1778..5d8ce7ea 100644 --- a/tests/core_language/test_printer.py +++ b/graphql/language/tests/test_printer.py @@ -2,9 +2,9 @@ from pytest import raises -from graphql.core.language.ast import Field, Name -from graphql.core.language.parser import parse -from graphql.core.language.printer import print_ast +from graphql.language.ast import Field, Name +from graphql.language.parser import parse +from graphql.language.printer import print_ast from .fixtures import KITCHEN_SINK @@ -48,7 +48,7 @@ def test_correctly_prints_mutation_operation_without_name(): def test_correctly_prints_query_with_artifacts(): query_ast_shorthanded = parse( - 'query ($foo: TestType) @testDirective { id, name }' + 'query ($foo: TestType) @testDirective { id, name }' ) assert print_ast(query_ast_shorthanded) == '''query ($foo: TestType) @testDirective { id @@ -59,7 +59,7 @@ def test_correctly_prints_query_with_artifacts(): def test_correctly_prints_mutation_with_artifacts(): query_ast_shorthanded = parse( - 'mutation ($foo: TestType) @testDirective { id, name }' + 'mutation ($foo: TestType) @testDirective { id, name }' ) assert print_ast(query_ast_shorthanded) == '''mutation ($foo: TestType) @testDirective { id diff --git a/tests/core_language/test_schema_parser.py b/graphql/language/tests/test_schema_parser.py similarity index 98% rename from tests/core_language/test_schema_parser.py rename to graphql/language/tests/test_schema_parser.py index bfe7ac67..f9a853a5 100644 --- a/tests/core_language/test_schema_parser.py +++ b/graphql/language/tests/test_schema_parser.py @@ -1,9 +1,9 @@ from pytest import raises -from graphql.core import Source, parse -from graphql.core.language import ast -from graphql.core.language.error import LanguageError -from graphql.core.language.parser import Loc +from graphql import Source, parse +from graphql.error import GraphQLSyntaxError +from graphql.language import ast +from graphql.language.parser import Loc def create_loc_fn(body): @@ -689,7 +689,7 @@ def test_parsing_simple_input_object_with_args_should_fail(): world(foo: Int): String } ''' - with raises(LanguageError) as excinfo: + with raises(GraphQLSyntaxError) as excinfo: parse(body) assert 'Syntax Error GraphQL (3:8) Expected :, found (' in excinfo.value.message diff --git a/tests/core_language/test_schema_printer.py b/graphql/language/tests/test_schema_printer.py similarity index 78% rename from tests/core_language/test_schema_printer.py rename to graphql/language/tests/test_schema_printer.py index 9064386f..abe7887d 100644 --- a/tests/core_language/test_schema_printer.py +++ b/graphql/language/tests/test_schema_printer.py @@ -2,9 +2,9 @@ from pytest import raises -from graphql.core import parse -from graphql.core.language import ast -from graphql.core.language.printer import print_ast +from graphql import parse +from graphql.language import ast +from graphql.language.printer import print_ast from .fixtures import SCHEMA_KITCHEN_SINK @@ -36,7 +36,12 @@ def test_prints_kitchen_sink(): ast = parse(SCHEMA_KITCHEN_SINK) printed = print_ast(ast) - expected = '''type Foo implements Bar { + expected = '''schema { + query: QueryType + mutation: MutationType +} + +type Foo implements Bar { one: Type two(argument: InputType!): Type three(argument: InputType, other: String): Int @@ -67,6 +72,10 @@ def test_prints_kitchen_sink(): extend type Foo { seven(argument: [String]): Type } + +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT ''' assert printed == expected diff --git a/tests/core_language/test_visitor.py b/graphql/language/tests/test_visitor.py similarity index 92% rename from tests/core_language/test_visitor.py rename to graphql/language/tests/test_visitor.py index 17cbbb67..5ede5294 100644 --- a/tests/core_language/test_visitor.py +++ b/graphql/language/tests/test_visitor.py @@ -1,13 +1,95 @@ -from graphql.core.language.ast import Field, Name, SelectionSet -from graphql.core.language.parser import parse -from graphql.core.language.printer import print_ast -from graphql.core.language.visitor import ( - BREAK, REMOVE, Visitor, visit, ParallelVisitor, TypeInfoVisitor) -from graphql.core.utils.type_info import TypeInfo -from graphql.core.type import is_composite_type, get_named_type - +from graphql.language.ast import (Document, Field, Name, OperationDefinition, + SelectionSet) +from graphql.language.parser import parse +from graphql.language.printer import print_ast +from graphql.language.visitor import (BREAK, REMOVE, ParallelVisitor, + TypeInfoVisitor, Visitor, visit) +from graphql.type import get_named_type, is_composite_type +from graphql.utils.type_info import TypeInfo + +from ...validation.tests.utils import test_schema from .fixtures import KITCHEN_SINK -from ..core_validation.utils import test_schema + + +def test_allows_editing_a_node_both_on_enter_and_on_leave(): + ast = parse('{ a, b, c { a, b, c } }', no_location=True) + + class TestVisitor(Visitor): + + def __init__(self): + self.did_enter = False + self.did_leave = False + + def enter(self, node, *args): + if isinstance(node, OperationDefinition): + self.did_enter = True + selection_set = node.selection_set + self.selections = None + if selection_set: + self.selections = selection_set.selections + new_selection_set = SelectionSet( + selections=[]) + return OperationDefinition( + name=node.name, + variable_definitions=node.variable_definitions, + directives=node.directives, + loc=node.loc, + operation=node.operation, + selection_set=new_selection_set) + + def leave(self, node, *args): + if isinstance(node, OperationDefinition): + self.did_leave = True + new_selection_set = None + if self.selections: + new_selection_set = SelectionSet( + selections=self.selections) + return OperationDefinition( + name=node.name, + variable_definitions=node.variable_definitions, + directives=node.directives, + loc=node.loc, + operation=node.operation, + selection_set=new_selection_set) + + visitor = TestVisitor() + edited_ast = visit(ast, visitor) + assert ast == parse('{ a, b, c { a, b, c } }', no_location=True) + assert edited_ast == ast + assert visitor.did_enter + assert visitor.did_leave + + +def test_allows_editing_the_root_node_on_enter_and_on_leave(): + ast = parse('{ a, b, c { a, b, c } }', no_location=True) + + definitions = ast.definitions + + class TestVisitor(Visitor): + + def __init__(self): + self.did_enter = False + self.did_leave = False + + def enter(self, node, *args): + if isinstance(node, Document): + self.did_enter = True + return Document( + loc=node.loc, + definitions=[]) + + def leave(self, node, *args): + if isinstance(node, Document): + self.did_leave = True + return Document( + loc=node.loc, + definitions=definitions) + + visitor = TestVisitor() + edited_ast = visit(ast, visitor) + assert edited_ast == ast + assert visitor.did_enter + assert visitor.did_leave def test_allows_for_editing_on_enter(): diff --git a/tests/core_language/test_visitor_meta.py b/graphql/language/tests/test_visitor_meta.py similarity index 92% rename from tests/core_language/test_visitor_meta.py rename to graphql/language/tests/test_visitor_meta.py index a57f9f99..3e0dd4e1 100644 --- a/tests/core_language/test_visitor_meta.py +++ b/graphql/language/tests/test_visitor_meta.py @@ -1,5 +1,5 @@ -from graphql.core.language import ast -from graphql.core.language.visitor import Visitor +from graphql.language import ast +from graphql.language.visitor import Visitor def test_visitor_meta_creates_enter_and_leave_handlers(): diff --git a/graphql/core/language/visitor.py b/graphql/language/visitor.py similarity index 99% rename from graphql/core/language/visitor.py rename to graphql/language/visitor.py index cb166a20..5c3578c3 100644 --- a/graphql/core/language/visitor.py +++ b/graphql/language/visitor.py @@ -7,6 +7,7 @@ class Falsey(object): + def __nonzero__(self): return False @@ -153,7 +154,7 @@ def visit(root, visitor, key_map=None): break if edits: - new_root = edits[0][1] + new_root = edits[-1][1] return new_root diff --git a/graphql/core/language/visitor_meta.py b/graphql/language/visitor_meta.py similarity index 94% rename from graphql/core/language/visitor_meta.py rename to graphql/language/visitor_meta.py index d540f513..1cf080aa 100644 --- a/graphql/core/language/visitor_meta.py +++ b/graphql/language/visitor_meta.py @@ -30,16 +30,22 @@ ast.ListType: ('type',), ast.NonNullType: ('type',), + ast.SchemaDefinition: ('operation_types',), + ast.OperationTypeDefinition: ('type',), + + ast.ScalarTypeDefinition: ('name',), ast.ObjectTypeDefinition: ('name', 'interfaces', 'fields'), ast.FieldDefinition: ('name', 'arguments', 'type'), ast.InputValueDefinition: ('name', 'type', 'default_value'), ast.InterfaceTypeDefinition: ('name', 'fields'), ast.UnionTypeDefinition: ('name', 'types'), - ast.ScalarTypeDefinition: ('name',), ast.EnumTypeDefinition: ('name', 'values'), ast.EnumValueDefinition: ('name',), ast.InputObjectTypeDefinition: ('name', 'fields'), + ast.TypeExtensionDefinition: ('definition',), + + ast.DirectiveDefinition: ('name', 'arguments', 'locations'), } AST_KIND_TO_TYPE = {c.__name__: c for c in QUERY_DOCUMENT_KEYS.keys()} diff --git a/tests/core_language/__init__.py b/graphql/pyutils/__init__.py similarity index 100% rename from tests/core_language/__init__.py rename to graphql/pyutils/__init__.py diff --git a/graphql/pyutils/contain_subset.py b/graphql/pyutils/contain_subset.py new file mode 100644 index 00000000..ae8e7535 --- /dev/null +++ b/graphql/pyutils/contain_subset.py @@ -0,0 +1,25 @@ +obj = (dict, list, tuple) + + +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)): + return False + if not isinstance(expected, obj) or expected is None: + return expected == actual + if expected and not actual: + return False + if isinstance(expected, list): + aa = actual[:] + return all([any([contain_subset(exp, act) for act in aa]) for exp in expected]) + for key in expected.keys(): + eo = expected[key] + ao = actual.get(key) + if isinstance(eo, obj) and eo is not None and ao is not None: + if not contain_subset(eo, ao): + return False + continue + if ao != eo: + return False + return True diff --git a/graphql/core/pyutils/default_ordered_dict.py b/graphql/pyutils/default_ordered_dict.py similarity index 100% rename from graphql/core/pyutils/default_ordered_dict.py rename to graphql/pyutils/default_ordered_dict.py index e4402621..e82a1be1 100644 --- a/graphql/core/pyutils/default_ordered_dict.py +++ b/graphql/pyutils/default_ordered_dict.py @@ -1,5 +1,5 @@ -from collections import OrderedDict import copy +from collections import OrderedDict class DefaultOrderedDict(OrderedDict): diff --git a/graphql/core/pyutils/pair_set.py b/graphql/pyutils/pair_set.py similarity index 100% rename from graphql/core/pyutils/pair_set.py rename to graphql/pyutils/pair_set.py diff --git a/tests/core_pyutils/__init__.py b/graphql/pyutils/tests/__init__.py similarity index 100% rename from tests/core_pyutils/__init__.py rename to graphql/pyutils/tests/__init__.py diff --git a/graphql/pyutils/tests/test_contain_subset.py b/graphql/pyutils/tests/test_contain_subset.py new file mode 100644 index 00000000..11b29cfc --- /dev/null +++ b/graphql/pyutils/tests/test_contain_subset.py @@ -0,0 +1,127 @@ +from ..contain_subset import contain_subset + +plain_object = { + 'a': 'b', + 'c': 'd' +} + +complex_object = { + 'a': 'b', + 'c': 'd', + 'e': { + 'foo': 'bar', + 'baz': { + 'qux': 'quux' + } + } +} + + +def test_plain_object_should_pass_for_smaller_object(): + assert contain_subset({'a': 'b'}, plain_object) + + +def test_plain_object_should_pass_for_same_object(): + assert contain_subset({ + 'a': 'b', + 'c': 'd' + }, plain_object) + + +def test_plain_object_should_reject_for_similar_object(): + assert not contain_subset({ + 'a': 'notB', + 'c': 'd' + }, plain_object) + + +def test_complex_object_should_pass_for_smaller_object(): + assert contain_subset({ + 'a': 'b', + 'e': { + 'foo': 'bar' + } + }, complex_object) + + +def test_complex_object_should_pass_for_smaller_object_other(): + assert contain_subset({ + 'e': { + 'foo': 'bar', + 'baz': { + 'qux': 'quux' + } + } + }, complex_object) + + +def test_complex_object_should_pass_for_same_object(): + assert contain_subset({ + 'a': 'b', + 'c': 'd', + 'e': { + 'foo': 'bar', + 'baz': { + 'qux': 'quux' + } + } + }, complex_object) + + +def test_complex_object_should_reject_for_similar_object(): + assert not contain_subset({ + 'e': { + 'foo': 'bar', + 'baz': { + 'qux': 'notAQuux' + } + } + }, complex_object) + + +def test_circular_objects_should_contain_subdocument(): + obj = {} + obj['arr'] = [obj, obj] + obj['arr'].append(obj['arr']) + obj['obj'] = obj + + assert contain_subset({ + 'arr': [ + {'arr': []}, + {'arr': []}, + [ + {'arr': []}, + {'arr': []} + ] + ] + }, obj) + + +def test_circular_objects_should_not_contain_similardocument(): + obj = {} + obj['arr'] = [obj, obj] + obj['arr'].append(obj['arr']) + obj['obj'] = obj + + assert not contain_subset({ + 'arr': [ + {'arr': ['just random field']}, + {'arr': []}, + [ + {'arr': []}, + {'arr': []} + ] + ] + }, obj) + + +def test_should_contain_others(): + obj = { + 'elems': [{'a': 'b', 'c': 'd', 'e': 'f'}, {'g': 'h'}] + } + assert contain_subset({ + 'elems': [{ + 'g': 'h' + }, {'a': 'b', 'e': 'f'} + ] + }, obj) diff --git a/tests/core_pyutils/test_default_ordered_dict.py b/graphql/pyutils/tests/test_default_ordered_dict.py similarity index 96% rename from tests/core_pyutils/test_default_ordered_dict.py rename to graphql/pyutils/tests/test_default_ordered_dict.py index a8d5fa79..f1069137 100644 --- a/tests/core_pyutils/test_default_ordered_dict.py +++ b/graphql/pyutils/tests/test_default_ordered_dict.py @@ -3,7 +3,7 @@ from pytest import raises -from graphql.core.pyutils.default_ordered_dict import DefaultOrderedDict +from graphql.pyutils.default_ordered_dict import DefaultOrderedDict def test_will_missing_will_set_value_from_factory(): diff --git a/tests/core_pyutils/test_pair_set.py b/graphql/pyutils/tests/test_pair_set.py similarity index 90% rename from tests/core_pyutils/test_pair_set.py rename to graphql/pyutils/tests/test_pair_set.py index fc19e48d..9bc3610b 100644 --- a/tests/core_pyutils/test_pair_set.py +++ b/graphql/pyutils/tests/test_pair_set.py @@ -1,4 +1,4 @@ -from graphql.core.pyutils.pair_set import PairSet +from graphql.pyutils.pair_set import PairSet def test_pair_set(): diff --git a/graphql/core/type/__init__.py b/graphql/type/__init__.py similarity index 87% rename from graphql/core/type/__init__.py rename to graphql/type/__init__.py index 1a5b526a..3b71a58a 100644 --- a/graphql/core/type/__init__.py +++ b/graphql/type/__init__.py @@ -17,8 +17,13 @@ is_composite_type, is_input_type, is_leaf_type, + is_type, + get_nullable_type, is_output_type ) +from .directives import ( + GraphQLDirective +) from .scalars import ( # no import order GraphQLInt, GraphQLFloat, diff --git a/graphql/core/type/definition.py b/graphql/type/definition.py similarity index 89% rename from graphql/core/type/definition.py rename to graphql/type/definition.py index 430fcfbe..3b8cbe8e 100644 --- a/graphql/core/type/definition.py +++ b/graphql/type/definition.py @@ -1,8 +1,8 @@ import collections import copy -import re from ..language import ast +from ..utils.assert_valid_name import assert_valid_name def is_type(type): @@ -157,7 +157,7 @@ class GraphQLObjectType(GraphQLType): 'street': GraphQLField(GraphQLString), 'number': GraphQLField(GraphQLInt), 'formatted': GraphQLField(GraphQLString, - resolver=lambda obj, args, info: obj.number + ' ' + obj.street), + resolver=lambda obj, args, context, info: obj.number + ' ' + obj.street), }) When two types need to refer to each other, or a type needs to refer to @@ -187,7 +187,6 @@ def __init__(self, name, fields, interfaces=None, is_type_of=None, description=N self._provided_interfaces = interfaces self._field_map = None self._interfaces = None - add_impl_to_interfaces(self) def get_fields(self): if self._field_map is None: @@ -278,7 +277,7 @@ def define_interfaces(type, interfaces): '{} may only implement Interface types, it cannot implement: {}.'.format(type, interface) ) - if not callable(interface.type_resolver): + if not callable(interface.resolve_type): assert callable(type.is_type_of), ( 'Interface Type {} does not provide a "resolve_type" function ' 'and implementing Type {} does not provide a "is_type_of" ' @@ -289,11 +288,6 @@ def define_interfaces(type, interfaces): return interfaces -def add_impl_to_interfaces(impl): - for type in impl.get_interfaces(): - type._impls.append(impl) - - class GraphQLField(object): __slots__ = 'name', 'type', 'args', 'resolver', 'deprecation_reason', 'description' @@ -360,7 +354,7 @@ class GraphQLInterfaceType(GraphQLType): 'name': GraphQLField(GraphQLString), }) """ - __slots__ = 'name', 'description', 'type_resolver', '_fields', '_impls', '_field_map', '_possible_type_names' + __slots__ = 'name', 'description', 'resolve_type', '_fields', '_field_map' def __init__(self, name, fields=None, resolve_type=None, description=None): assert name, 'Type must be named.' @@ -371,12 +365,10 @@ def __init__(self, name, fields=None, resolve_type=None, description=None): if resolve_type is not None: assert callable(resolve_type), '{} must provide "resolve_type" as a function.'.format(self) - self.type_resolver = resolve_type + self.resolve_type = resolve_type self._fields = fields - self._impls = [] self._field_map = None - self._possible_type_names = None def get_fields(self): if self._field_map is None: @@ -384,29 +376,6 @@ def get_fields(self): return self._field_map - def get_possible_types(self): - return self._impls - - def is_possible_type(self, type): - if self._possible_type_names is None: - self._possible_type_names = set( - t.name for t in self.get_possible_types() - ) - return type.name in self._possible_type_names - - def resolve_type(self, value, info): - if self.type_resolver: - return self.type_resolver(value, info) - - return get_type_of(value, info, self) - - -def get_type_of(value, info, abstract_type): - possible_types = abstract_type.get_possible_types() - for type in possible_types: - if callable(type.is_type_of) and type.is_type_of(value, info): - return type - class GraphQLUnionType(GraphQLType): """Union Type Definition @@ -426,7 +395,7 @@ def resolve_type(self, value): if isinstance(value, Cat): return CatType() """ - __slots__ = 'name', 'description', '_resolve_type', '_types', '_possible_type_names', '_possible_types' + __slots__ = 'name', 'description', 'resolve_type', '_types' def __init__(self, name, types=None, resolve_type=None, description=None): assert name, 'Type must be named.' @@ -437,30 +406,11 @@ def __init__(self, name, types=None, resolve_type=None, description=None): if resolve_type is not None: assert callable(resolve_type), '{} must provide "resolve_type" as a function.'.format(self) - self._resolve_type = resolve_type - self._types = types - self._possible_types = None - self._possible_type_names = None + self.resolve_type = resolve_type + self._types = define_types(self, types) - def get_possible_types(self): - if self._possible_types is None: - self._possible_types = define_types(self, self._types) - - return self._possible_types - - def is_possible_type(self, type): - if self._possible_type_names is None: - self._possible_type_names = set( - t.name for t in self.get_possible_types() - ) - - return type.name in self._possible_type_names - - def resolve_type(self, value, info): - if self._resolve_type: - return self._resolve_type(value, info) - - return get_type_of(value, info, self) + def get_types(self): + return self._types def define_types(union_type, types): @@ -469,7 +419,7 @@ def define_types(union_type, types): assert isinstance(types, (list, tuple)) and len( types) > 0, 'Must provide types for Union {}.'.format(union_type.name) - has_resolve_type_fn = callable(union_type._resolve_type) + has_resolve_type_fn = callable(union_type.resolve_type) for type in types: assert isinstance(type, GraphQLObjectType), ( @@ -495,11 +445,14 @@ class GraphQLEnumType(GraphQLType): Example: - RGBType = GraphQLEnumType('RGB', { - 'RED': 0, - 'GREEN': 1, - 'BLUE': 2, - }) + RGBType = GraphQLEnumType( + name='RGB', + values=OrderedDict([ + ('RED', GraphQLEnumValue(0)), + ('GREEN', GraphQLEnumValue(1)), + ('BLUE', GraphQLEnumValue(2)) + ]) + ) Note: If a value is not provided in a definition, the name of the enum value will be used as it's internal value. """ @@ -751,11 +704,3 @@ def __str__(self): def is_same_type(self, other): return isinstance(other, GraphQLNonNull) and self.of_type.is_same_type(other.of_type) - - -NAME_PATTERN = r'^[_a-zA-Z][_a-zA-Z0-9]*$' -COMPILED_NAME_PATTERN = re.compile(NAME_PATTERN) - - -def assert_valid_name(name): - assert COMPILED_NAME_PATTERN.match(name), 'Names must match /{}/ but "{}" does not.'.format(NAME_PATTERN, name) diff --git a/graphql/type/directives.py b/graphql/type/directives.py new file mode 100644 index 00000000..e0da3d99 --- /dev/null +++ b/graphql/type/directives.py @@ -0,0 +1,97 @@ +import collections + +from ..utils.assert_valid_name import assert_valid_name +from .definition import GraphQLArgument, GraphQLNonNull, is_input_type +from .scalars import GraphQLBoolean + + +class DirectiveLocation(object): + QUERY = 'QUERY' + MUTATION = 'MUTATION' + SUBSCRIPTION = 'SUBSCRIPTION' + FIELD = 'FIELD' + FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION' + FRAGMENT_SPREAD = 'FRAGMENT_SPREAD' + INLINE_FRAGMENT = 'INLINE_FRAGMENT' + + OPERATION_LOCATIONS = [ + QUERY, + MUTATION, + SUBSCRIPTION + ] + + FRAGMENT_LOCATIONS = [ + FRAGMENT_DEFINITION, + FRAGMENT_SPREAD, + INLINE_FRAGMENT + ] + + FIELD_LOCATIONS = [ + FIELD + ] + + +class GraphQLDirective(object): + __slots__ = 'name', 'args', 'description', 'locations' + + def __init__(self, name, description=None, args=None, locations=None): + assert name, 'Directive must be named.' + assert_valid_name(name) + assert isinstance(locations, collections.Iterable), 'Must provide locations for directive.' + + self.name = name + self.description = description + self.locations = locations + + self.args = [] + if args: + assert isinstance(args, dict), '{} args must be a dict with argument names as keys.'.format(name) + for arg_name, _arg in args.items(): + assert_valid_name(arg_name) + assert is_input_type(_arg.type), '{}({}) argument type must be Input Type but got {}.'.format( + name, + arg_name, + _arg.type) + self.args.append(arg( + arg_name, + description=_arg.description, + type=_arg.type, + default_value=_arg.default_value + )) + + +def arg(name, *args, **kwargs): + a = GraphQLArgument(*args, **kwargs) + a.name = name + return a + + +GraphQLIncludeDirective = GraphQLDirective( + name='include', + args={ + 'if': GraphQLArgument( + type=GraphQLNonNull(GraphQLBoolean), + description='Included when true.', + ), + }, + locations=[ + DirectiveLocation.FIELD, + DirectiveLocation.FRAGMENT_SPREAD, + DirectiveLocation.INLINE_FRAGMENT, + ] +) + +GraphQLSkipDirective = GraphQLDirective( + name='skip', + args={ + 'if': GraphQLArgument( + type=GraphQLNonNull(GraphQLBoolean), + description='Skipped when true.', + ), + }, + locations=[ + DirectiveLocation.FIELD, + DirectiveLocation.FRAGMENT_SPREAD, + DirectiveLocation.INLINE_FRAGMENT, + ] +) diff --git a/graphql/core/type/introspection.py b/graphql/type/introspection.py similarity index 82% rename from graphql/core/type/introspection.py rename to graphql/type/introspection.py index ed916c2b..440df0b7 100644 --- a/graphql/core/type/introspection.py +++ b/graphql/type/introspection.py @@ -7,6 +7,7 @@ GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLUnionType) +from .directives import DirectiveLocation from .scalars import GraphQLBoolean, GraphQLString __Schema = GraphQLObjectType( @@ -44,6 +45,10 @@ )), ])) +_on_operation_locations = set(DirectiveLocation.OPERATION_LOCATIONS) +_on_fragment_locations = set(DirectiveLocation.FRAGMENT_LOCATIONS) +_on_field_locations = set(DirectiveLocation.FIELD_LOCATIONS) + __Directive = GraphQLObjectType( '__Directive', description='A Directive provides a way to describe alternate runtime execution and ' @@ -55,24 +60,67 @@ fields=lambda: OrderedDict([ ('name', GraphQLField(GraphQLNonNull(GraphQLString))), ('description', GraphQLField(GraphQLString)), + ('locations', GraphQLField( + type=GraphQLNonNull(GraphQLList(GraphQLNonNull(__DirectiveLocation))), + )), ('args', GraphQLField( type=GraphQLNonNull(GraphQLList(GraphQLNonNull(__InputValue))), resolver=lambda directive, *args: directive.args or [], )), ('onOperation', GraphQLField( type=GraphQLNonNull(GraphQLBoolean), - resolver=lambda directive, *args: directive.on_operation, + deprecation_reason='Use `locations`.', + resolver=lambda directive, *args: set(directive.locations) & _on_operation_locations, )), ('onFragment', GraphQLField( type=GraphQLNonNull(GraphQLBoolean), - resolver=lambda directive, *args: directive.on_fragment, + deprecation_reason='Use `locations`.', + resolver=lambda directive, *args: set(directive.locations) & _on_fragment_locations, )), ('onField', GraphQLField( type=GraphQLNonNull(GraphQLBoolean), - resolver=lambda directive, *args: directive.on_field, + deprecation_reason='Use `locations`.', + resolver=lambda directive, *args: set(directive.locations) & _on_field_locations, )) ])) +__DirectiveLocation = GraphQLEnumType( + '__DirectiveLocation', + description=( + 'A Directive can be adjacent to many parts of the GraphQL language, a ' + + '__DirectiveLocation describes one such possible adjacencies.' + ), + values=OrderedDict([ + ('QUERY', GraphQLEnumValue( + DirectiveLocation.QUERY, + description='Location adjacent to a query operation.' + )), + ('MUTATION', GraphQLEnumValue( + DirectiveLocation.MUTATION, + description='Location adjacent to a mutation operation.' + )), + ('SUBSCRIPTION', GraphQLEnumValue( + DirectiveLocation.SUBSCRIPTION, + description='Location adjacent to a subscription operation.' + )), + ('FIELD', GraphQLEnumValue( + DirectiveLocation.FIELD, + description='Location adjacent to a field.' + )), + ('FRAGMENT_DEFINITION', GraphQLEnumValue( + DirectiveLocation.FRAGMENT_DEFINITION, + description='Location adjacent to a fragment definition.' + )), + ('FRAGMENT_SPREAD', GraphQLEnumValue( + DirectiveLocation.FRAGMENT_SPREAD, + description='Location adjacent to a fragment spread.' + )), + ('INLINE_FRAGMENT', GraphQLEnumValue( + DirectiveLocation.INLINE_FRAGMENT, + description='Location adjacent to an inline fragment.' + )), + ])) + class TypeKind(object): SCALAR = 'SCALAR' @@ -120,9 +168,9 @@ def interfaces(type, *_): return type.get_interfaces() @staticmethod - def possible_types(type, *_): + def possible_types(type, args, context, info): if isinstance(type, (GraphQLInterfaceType, GraphQLUnionType)): - return type.get_possible_types() + return info.schema.get_possible_types(type) @staticmethod def enum_values(type, args, *_): @@ -302,7 +350,7 @@ def input_fields(type, *_): SchemaMetaFieldDef = GraphQLField( type=GraphQLNonNull(__Schema), description='Access the current type schema of this server.', - resolver=lambda source, args, info: info.schema, + resolver=lambda source, args, context, info: info.schema, args=[] ) SchemaMetaFieldDef.name = '__schema' @@ -313,7 +361,7 @@ def input_fields(type, *_): type=__Type, description='Request the type information of a single type.', args=[TypeMetaFieldDef_args_name], - resolver=lambda source, args, info: info.schema.get_type(args['name']) + resolver=lambda source, args, context, info: info.schema.get_type(args['name']) ) TypeMetaFieldDef.name = '__type' del TypeMetaFieldDef_args_name @@ -321,7 +369,7 @@ def input_fields(type, *_): TypeNameMetaFieldDef = GraphQLField( type=GraphQLNonNull(GraphQLString), description='The name of the current Object type at runtime.', - resolver=lambda source, args, info: info.parent_type.name, + resolver=lambda source, args, context, info: info.parent_type.name, args=[] ) TypeNameMetaFieldDef.name = '__typename' diff --git a/graphql/core/type/scalars.py b/graphql/type/scalars.py similarity index 100% rename from graphql/core/type/scalars.py rename to graphql/type/scalars.py diff --git a/graphql/core/type/schema.py b/graphql/type/schema.py similarity index 72% rename from graphql/core/type/schema.py rename to graphql/type/schema.py index d3045766..f9d53c5b 100644 --- a/graphql/core/type/schema.py +++ b/graphql/type/schema.py @@ -1,4 +1,5 @@ -from collections import OrderedDict +from collections import Iterable, OrderedDict, defaultdict +from functools import reduce from ..utils.type_comparators import is_equal_type, is_type_sub_type_of from .definition import (GraphQLInputObjectType, GraphQLInterfaceType, @@ -22,9 +23,9 @@ class GraphQLSchema(object): mutation=MyAppMutationRootType ) """ - __slots__ = '_query', '_mutation', '_subscription', '_type_map', '_directives', + __slots__ = '_query', '_mutation', '_subscription', '_type_map', '_directives', '_implementations', '_possible_type_map' - def __init__(self, query, mutation=None, subscription=None, directives=None): + def __init__(self, query, mutation=None, subscription=None, directives=None, types=None): assert isinstance(query, GraphQLObjectType), 'Schema query must be Object Type but got: {}.'.format(query) if mutation: assert isinstance(mutation, GraphQLObjectType), \ @@ -34,11 +35,13 @@ def __init__(self, query, mutation=None, subscription=None, directives=None): assert isinstance(subscription, GraphQLObjectType), \ 'Schema subscription must be Object Type but got: {}.'.format(subscription) + if types: + assert isinstance(types, Iterable), \ + 'Schema types must be iterable if provided but got: {}.'.format(types) + self._query = query self._mutation = mutation self._subscription = subscription - self._type_map = self._build_type_map() - if directives is None: directives = [ GraphQLIncludeDirective, @@ -51,11 +54,20 @@ def __init__(self, query, mutation=None, subscription=None, directives=None): ) self._directives = directives + self._possible_type_map = defaultdict(set) + self._type_map = self._build_type_map(types) + # Keep track of all implementations by interface name. + self._implementations = defaultdict(list) + for type in self._type_map.values(): + if isinstance(type, GraphQLObjectType): + for interface in type.get_interfaces(): + self._implementations[interface.name].append(type) + # Enforce correct interface implementations. for type in self._type_map.values(): if isinstance(type, GraphQLObjectType): for interface in type.get_interfaces(): - assert_object_implements_interface(type, interface) + assert_object_implements_interface(self, type, interface) def get_query_type(self): return self._query @@ -82,14 +94,32 @@ def get_directive(self, name): return None - def _build_type_map(self): - type_map = OrderedDict() - types = (self.get_query_type(), self.get_mutation_type(), self.get_subscription_type(), IntrospectionSchema) - for type in types: - type_map = type_map_reducer(type_map, type) - + def _build_type_map(self, _types): + types = [ + self.get_query_type(), + self.get_mutation_type(), + self.get_subscription_type(), + IntrospectionSchema + ] + if _types: + types += _types + + type_map = reduce(type_map_reducer, types, OrderedDict()) return type_map + def get_possible_types(self, abstract_type): + if isinstance(abstract_type, GraphQLUnionType): + return abstract_type.get_types() + assert isinstance(abstract_type, GraphQLInterfaceType) + return self._implementations[abstract_type.name] + + def is_possible_type(self, abstract_type, possible_type): + if not self._possible_type_map[abstract_type.name]: + possible_types = self.get_possible_types(abstract_type) + self._possible_type_map[abstract_type.name].update([p.name for p in possible_types]) + + return possible_type.name in self._possible_type_map[abstract_type.name] + def type_map_reducer(map, type): if not type: @@ -109,8 +139,8 @@ def type_map_reducer(map, type): reduced_map = map - if isinstance(type, (GraphQLUnionType, GraphQLInterfaceType)): - for t in type.get_possible_types(): + if isinstance(type, (GraphQLUnionType)): + for t in type.get_types(): reduced_map = type_map_reducer(reduced_map, t) if isinstance(type, GraphQLObjectType): @@ -131,7 +161,7 @@ def type_map_reducer(map, type): return reduced_map -def assert_object_implements_interface(object, interface): +def assert_object_implements_interface(schema, object, interface): object_field_map = object.get_fields() interface_field_map = interface.get_fields() @@ -142,7 +172,7 @@ def assert_object_implements_interface(object, interface): interface, field_name, object ) - assert is_type_sub_type_of(object_field.type, interface_field.type), ( + assert is_type_sub_type_of(schema, object_field.type, interface_field.type), ( '{}.{} expects type "{}" but {}.{} provides type "{}".' ).format(interface, field_name, interface_field.type, object, field_name, object_field.type) diff --git a/tests/core_starwars/__init__.py b/graphql/type/tests/__init__.py similarity index 100% rename from tests/core_starwars/__init__.py rename to graphql/type/tests/__init__.py diff --git a/tests/core_type/test_definition.py b/graphql/type/tests/test_definition.py similarity index 93% rename from tests/core_type/test_definition.py rename to graphql/type/tests/test_definition.py index 0f7eb9ac..c3cb047a 100644 --- a/tests/core_type/test_definition.py +++ b/graphql/type/tests/test_definition.py @@ -2,13 +2,13 @@ from py.test import raises -from graphql.core.type import (GraphQLArgument, GraphQLBoolean, - GraphQLEnumType, GraphQLEnumValue, GraphQLField, - GraphQLInputObjectField, GraphQLInputObjectType, - GraphQLInt, GraphQLInterfaceType, GraphQLList, - GraphQLNonNull, GraphQLObjectType, - GraphQLSchema, GraphQLString, GraphQLUnionType) -from graphql.core.type.definition import is_input_type, is_output_type +from graphql.type import (GraphQLArgument, GraphQLBoolean, GraphQLEnumType, + GraphQLEnumValue, GraphQLField, + GraphQLInputObjectField, GraphQLInputObjectType, + GraphQLInt, GraphQLInterfaceType, GraphQLList, + GraphQLNonNull, GraphQLObjectType, GraphQLSchema, + GraphQLString, GraphQLUnionType) +from graphql.type.definition import is_input_type, is_output_type BlogImage = GraphQLObjectType('Image', { 'url': GraphQLField(GraphQLString), @@ -183,7 +183,7 @@ def test_includes_interfaces_thunk_subtypes_in_the_type_map(): fields={ 'iface': GraphQLField(SomeInterface) } - )) + ), types=[SomeSubtype]) assert schema.get_type_map()['SomeSubtype'] is SomeSubtype @@ -196,7 +196,12 @@ def test_includes_interfaces_subtypes_in_the_type_map(): interfaces=[SomeInterface], is_type_of=lambda: None ) - schema = GraphQLSchema(query=GraphQLObjectType(name='Query', fields={'iface': GraphQLField(SomeInterface)})) + schema = GraphQLSchema( + query=GraphQLObjectType( + name='Query', + fields={ + 'iface': GraphQLField(SomeInterface)}), + types=[SomeSubtype]) assert schema.get_type_map()['SomeSubtype'] == SomeSubtype diff --git a/tests/core_type/test_enum_type.py b/graphql/type/tests/test_enum_type.py similarity index 86% rename from tests/core_type/test_enum_type.py rename to graphql/type/tests/test_enum_type.py index d076de76..5fc36fca 100644 --- a/tests/core_type/test_enum_type.py +++ b/graphql/type/tests/test_enum_type.py @@ -2,10 +2,10 @@ from pytest import raises -from graphql.core import graphql -from graphql.core.type import (GraphQLArgument, GraphQLEnumType, - GraphQLEnumValue, GraphQLField, GraphQLInt, - GraphQLObjectType, GraphQLSchema, GraphQLString) +from graphql import graphql +from graphql.type import (GraphQLArgument, GraphQLEnumType, GraphQLEnumValue, + GraphQLField, GraphQLInt, GraphQLObjectType, + GraphQLSchema, GraphQLString) ColorType = GraphQLEnumType( name='Color', @@ -35,7 +35,7 @@ def get_first(args, *keys): 'fromInt': GraphQLArgument(GraphQLInt), 'fromString': GraphQLArgument(GraphQLString) }, - resolver=lambda value, args, info: get_first(args, 'fromInt', 'fromString', 'fromEnum') + resolver=lambda value, args, context, info: get_first(args, 'fromInt', 'fromString', 'fromEnum') ), 'colorInt': GraphQLField( type=GraphQLInt, @@ -43,7 +43,7 @@ def get_first(args, *keys): 'fromEnum': GraphQLArgument(ColorType), 'fromInt': GraphQLArgument(GraphQLInt), }, - resolver=lambda value, args, info: get_first(args, 'fromInt', 'fromEnum') + resolver=lambda value, args, context, info: get_first(args, 'fromInt', 'fromEnum') ) } ) @@ -56,7 +56,7 @@ def get_first(args, *keys): args={ 'color': GraphQLArgument(ColorType) }, - resolver=lambda value, args, info: args.get('color') + resolver=lambda value, args, context, info: args.get('color') ) } ) @@ -69,7 +69,7 @@ def get_first(args, *keys): args={ 'color': GraphQLArgument(ColorType) }, - resolver=lambda value, args, info: args.get('color') + resolver=lambda value, args, context, info: args.get('color') ) } ) @@ -130,40 +130,40 @@ def test_does_not_accept_enum_literal_in_place_of_int(): def test_accepts_json_string_as_enum_variable(): - result = graphql(Schema, 'query test($color: Color!) { colorEnum(fromEnum: $color) }', None, {'color': 'BLUE'}) + result = graphql(Schema, 'query test($color: Color!) { colorEnum(fromEnum: $color) }', variable_values={'color': 'BLUE'}) assert not result.errors assert result.data == {'colorEnum': 'BLUE'} def test_accepts_enum_literals_as_input_arguments_to_mutations(): - result = graphql(Schema, 'mutation x($color: Color!) { favoriteEnum(color: $color) }', None, {'color': 'GREEN'}) + result = graphql(Schema, 'mutation x($color: Color!) { favoriteEnum(color: $color) }', variable_values={'color': 'GREEN'}) assert not result.errors assert result.data == {'favoriteEnum': 'GREEN'} def test_accepts_enum_literals_as_input_arguments_to_subscriptions(): result = graphql( - Schema, 'subscription x($color: Color!) { subscribeToEnum(color: $color) }', None, { + Schema, 'subscription x($color: Color!) { subscribeToEnum(color: $color) }', variable_values={ 'color': 'GREEN'}) assert not result.errors assert result.data == {'subscribeToEnum': 'GREEN'} def test_does_not_accept_internal_value_as_enum_variable(): - result = graphql(Schema, 'query test($color: Color!) { colorEnum(fromEnum: $color) }', None, {'color': 2}) + result = graphql(Schema, 'query test($color: Color!) { colorEnum(fromEnum: $color) }', variable_values={'color': 2}) assert not result.data assert result.errors[0].message == 'Variable "$color" got invalid value 2.\n' \ 'Expected type "Color", found 2.' def test_does_not_accept_string_variables_as_enum_input(): - result = graphql(Schema, 'query test($color: String!) { colorEnum(fromEnum: $color) }', None, {'color': 'BLUE'}) + result = graphql(Schema, 'query test($color: String!) { colorEnum(fromEnum: $color) }', variable_values={'color': 'BLUE'}) assert not result.data assert result.errors[0].message == 'Variable "color" of type "String!" used in position expecting type "Color".' def test_does_not_accept_internal_value_as_enum_input(): - result = graphql(Schema, 'query test($color: Int!) { colorEnum(fromEnum: $color) }', None, {'color': 2}) + result = graphql(Schema, 'query test($color: Int!) { colorEnum(fromEnum: $color) }', variable_values={'color': 2}) assert not result.data assert result.errors[0].message == 'Variable "color" of type "Int!" used in position expecting type "Color".' diff --git a/graphql/type/tests/test_introspection.py b/graphql/type/tests/test_introspection.py new file mode 100644 index 00000000..e4633763 --- /dev/null +++ b/graphql/type/tests/test_introspection.py @@ -0,0 +1,1031 @@ +import json +from collections import OrderedDict + +from graphql import graphql +from graphql.error import format_error +from graphql.execution import execute +from graphql.language.parser import parse +from graphql.type import (GraphQLArgument, GraphQLEnumType, GraphQLEnumValue, + GraphQLField, GraphQLInputObjectField, + GraphQLInputObjectType, GraphQLList, + GraphQLObjectType, GraphQLSchema, GraphQLString) +from graphql.utils.introspection_query import introspection_query +from graphql.validation.rules import ProvidedNonNullArguments + +from ...pyutils.contain_subset import contain_subset + + +def test_executes_an_introspection_query(): + EmptySchema = GraphQLSchema(GraphQLObjectType('QueryRoot', {'f': GraphQLField(GraphQLString)})) + + result = graphql(EmptySchema, introspection_query) + assert not result.errors + expected = { + "__schema": { + "mutationType": None, + "subscriptionType": None, + "queryType": { + "name": "QueryRoot" + }, + "types": [{ + "kind": "OBJECT", + "name": "QueryRoot", + "inputFields": None, + "interfaces": [], + "enumValues": None, + "possibleTypes": None + }, { + "kind": "OBJECT", + "name": "__Schema", + "fields": [{ + "name": "types", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "LIST", + "name": None, + "ofType": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "OBJECT", + "name": "__Type" + } + } + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "queryType", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": None + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "mutationType", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": None + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "subscriptionType", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": None + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "directives", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "LIST", + "name": None, + "ofType": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "OBJECT", + "name": "__Directive" + } + } + } + }, + "isDeprecated": False, + "deprecationReason": None + }], + "inputFields": None, + "interfaces": [], + "enumValues": None, + "possibleTypes": None + }, { + "kind": "OBJECT", + "name": "__Type", + "fields": [{ + "name": "kind", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": None + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "name", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": None + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "description", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": None + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "fields", + "args": [{ + "name": "includeDeprecated", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": None + }, + "defaultValue": "false" + }], + "type": { + "kind": "LIST", + "name": None, + "ofType": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": None + } + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "interfaces", + "args": [], + "type": { + "kind": "LIST", + "name": None, + "ofType": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": None + } + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "possibleTypes", + "args": [], + "type": { + "kind": "LIST", + "name": None, + "ofType": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": None + } + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "enumValues", + "args": [{ + "name": "includeDeprecated", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": None + }, + "defaultValue": "false" + }], + "type": { + "kind": "LIST", + "name": None, + "ofType": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": None + } + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "inputFields", + "args": [], + "type": { + "kind": "LIST", + "name": None, + "ofType": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": None + } + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "ofType", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": None + }, + "isDeprecated": False, + "deprecationReason": None + }], + "inputFields": None, + "interfaces": [], + "enumValues": None, + "possibleTypes": None + }, { + "kind": "ENUM", + "name": "__TypeKind", + "fields": None, + "inputFields": None, + "interfaces": None, + "enumValues": [{ + "name": "SCALAR", + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "OBJECT", + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "INTERFACE", + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "UNION", + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "ENUM", + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "INPUT_OBJECT", + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "LIST", + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "NON_NULL", + "isDeprecated": False, + "deprecationReason": None + }], + "possibleTypes": None + }, { + "kind": "SCALAR", + "name": "String", + "fields": None, + "inputFields": None, + "interfaces": None, + "enumValues": None, + "possibleTypes": None + }, { + "kind": "SCALAR", + "name": "Boolean", + "fields": None, + "inputFields": None, + "interfaces": None, + "enumValues": None, + "possibleTypes": None + }, { + "kind": "OBJECT", + "name": "__Field", + "fields": [{ + "name": "name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": None + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "description", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": None + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "args", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "LIST", + "name": None, + "ofType": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue" + } + } + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "type", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": None + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "isDeprecated", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": None + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "deprecationReason", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": None + }, + "isDeprecated": False, + "deprecationReason": None + }], + "inputFields": None, + "interfaces": [], + "enumValues": None, + "possibleTypes": None + }, { + "kind": "OBJECT", + "name": "__InputValue", + "fields": [{ + "name": "name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": None + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "description", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": None + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "type", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": None + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "defaultValue", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": None + }, + "isDeprecated": False, + "deprecationReason": None + }], + "inputFields": None, + "interfaces": [], + "enumValues": None, + "possibleTypes": None + }, { + "kind": "OBJECT", + "name": "__EnumValue", + "fields": [{ + "name": "name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": None + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "description", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": None + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "isDeprecated", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": None + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "deprecationReason", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": None + }, + "isDeprecated": False, + "deprecationReason": None + }], + "inputFields": None, + "interfaces": [], + "enumValues": None, + "possibleTypes": None + }, { + "kind": "OBJECT", + "name": "__Directive", + "fields": [{ + "name": "name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": None + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "description", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": None + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "locations", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "LIST", + "name": None, + "ofType": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation" + } + } + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "args", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "LIST", + "name": None, + "ofType": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue" + } + } + } + }, + "isDeprecated": False, + "deprecationReason": None + }, { + "name": "onOperation", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": None + } + }, + "isDeprecated": True, + "deprecationReason": "Use `locations`." + }, { + "name": "onFragment", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": None + } + }, + "isDeprecated": True, + "deprecationReason": "Use `locations`." + }, { + "name": "onField", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": None + } + }, + "isDeprecated": True, + "deprecationReason": "Use `locations`." + }], + "inputFields": None, + "interfaces": [], + "enumValues": None, + "possibleTypes": None + }, { + "kind": "ENUM", + "name": "__DirectiveLocation", + "fields": None, + "inputFields": None, + "interfaces": None, + "enumValues": [{ + "name": "QUERY", + "isDeprecated": False + }, { + "name": "MUTATION", + "isDeprecated": False + }, { + "name": "SUBSCRIPTION", + "isDeprecated": False + }, { + "name": "FIELD", + "isDeprecated": False + }, { + "name": "FRAGMENT_DEFINITION", + "isDeprecated": False + }, { + "name": "FRAGMENT_SPREAD", + "isDeprecated": False + }, { + "name": "INLINE_FRAGMENT", + "isDeprecated": False + }], + "possibleTypes": None + }], + "directives": [{ + "name": "include", + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], + "args": [{ + "defaultValue": None, + "name": "if", + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": None + } + } + }] + }, { + "name": "skip", + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], + "args": [{ + "defaultValue": None, + "name": "if", + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": None + } + } + }] + }] + } + } + assert contain_subset(expected, result.data) + + +def test_introspects_on_input_object(): + TestInputObject = GraphQLInputObjectType('TestInputObject', OrderedDict([ + ('a', GraphQLInputObjectField(GraphQLString, default_value='foo')), + ('b', GraphQLInputObjectField(GraphQLList(GraphQLString))) + ])) + TestType = GraphQLObjectType('TestType', { + 'field': GraphQLField( + type=GraphQLString, + args={'complex': GraphQLArgument(TestInputObject)}, + resolver=lambda obj, args, context, info: json.dumps(args.get('complex')) + ) + }) + schema = GraphQLSchema(TestType) + request = ''' + { + __schema { + types { + kind + name + inputFields { + name + type { ...TypeRef } + defaultValue + } + } + } + } + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + ''' + result = graphql(schema, request) + assert not result.errors + assert {'kind': 'INPUT_OBJECT', + 'name': 'TestInputObject', + 'inputFields': + [{'name': 'a', + 'type': + {'kind': 'SCALAR', + 'name': 'String', + 'ofType': None}, + 'defaultValue': '"foo"'}, + {'name': 'b', + 'type': + {'kind': 'LIST', + 'name': None, + 'ofType': + {'kind': 'SCALAR', + 'name': 'String', + 'ofType': None}}, + 'defaultValue': None}]} in result.data['__schema']['types'] + + +def test_supports_the_type_root_field(): + TestType = GraphQLObjectType('TestType', { + 'testField': GraphQLField(GraphQLString) + }) + schema = GraphQLSchema(TestType) + request = '{ __type(name: "TestType") { name } }' + result = execute(schema, parse(request), object()) + assert not result.errors + assert result.data == {'__type': {'name': 'TestType'}} + + +def test_identifies_deprecated_fields(): + TestType = GraphQLObjectType('TestType', OrderedDict([ + ('nonDeprecated', GraphQLField(GraphQLString)), + ('deprecated', GraphQLField(GraphQLString, deprecation_reason='Removed in 1.0')) + ])) + schema = GraphQLSchema(TestType) + request = '''{__type(name: "TestType") { + name + fields(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } }''' + result = graphql(schema, request) + assert not result.errors + assert result.data == {'__type': { + 'name': 'TestType', + 'fields': [ + {'name': 'nonDeprecated', 'isDeprecated': False, 'deprecationReason': None}, + {'name': 'deprecated', 'isDeprecated': True, + 'deprecationReason': 'Removed in 1.0'}, + ] + }} + + +def test_respects_the_includedeprecated_parameter_for_fields(): + TestType = GraphQLObjectType('TestType', OrderedDict([ + ('nonDeprecated', GraphQLField(GraphQLString)), + ('deprecated', GraphQLField(GraphQLString, deprecation_reason='Removed in 1.0')) + ])) + schema = GraphQLSchema(TestType) + request = '''{__type(name: "TestType") { + name + trueFields: fields(includeDeprecated: true) { name } + falseFields: fields(includeDeprecated: false) { name } + omittedFields: fields { name } + } }''' + result = graphql(schema, request) + assert not result.errors + assert result.data == {'__type': { + 'name': 'TestType', + 'trueFields': [{'name': 'nonDeprecated'}, {'name': 'deprecated'}], + 'falseFields': [{'name': 'nonDeprecated'}], + 'omittedFields': [{'name': 'nonDeprecated'}], + }} + + +def test_identifies_deprecated_enum_values(): + TestEnum = GraphQLEnumType('TestEnum', OrderedDict([ + ('NONDEPRECATED', GraphQLEnumValue(0)), + ('DEPRECATED', GraphQLEnumValue(1, deprecation_reason='Removed in 1.0')), + ('ALSONONDEPRECATED', GraphQLEnumValue(2)) + ])) + TestType = GraphQLObjectType('TestType', { + 'testEnum': GraphQLField(TestEnum) + }) + schema = GraphQLSchema(TestType) + request = '''{__type(name: "TestEnum") { + name + enumValues(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } }''' + result = graphql(schema, request) + assert not result.errors + assert result.data == {'__type': { + 'name': 'TestEnum', + 'enumValues': [ + {'name': 'NONDEPRECATED', 'isDeprecated': False, 'deprecationReason': None}, + {'name': 'DEPRECATED', 'isDeprecated': True, 'deprecationReason': 'Removed in 1.0'}, + {'name': 'ALSONONDEPRECATED', 'isDeprecated': False, 'deprecationReason': None}, + ]}} + + +def test_respects_the_includedeprecated_parameter_for_enum_values(): + TestEnum = GraphQLEnumType('TestEnum', OrderedDict([ + ('NONDEPRECATED', GraphQLEnumValue(0)), + ('DEPRECATED', GraphQLEnumValue(1, deprecation_reason='Removed in 1.0')), + ('ALSONONDEPRECATED', GraphQLEnumValue(2)) + ])) + TestType = GraphQLObjectType('TestType', { + 'testEnum': GraphQLField(TestEnum) + }) + schema = GraphQLSchema(TestType) + request = '''{__type(name: "TestEnum") { + name + trueValues: enumValues(includeDeprecated: true) { name } + falseValues: enumValues(includeDeprecated: false) { name } + omittedValues: enumValues { name } + } }''' + result = graphql(schema, request) + assert not result.errors + assert result.data == {'__type': { + 'name': 'TestEnum', + 'trueValues': [{'name': 'NONDEPRECATED'}, {'name': 'DEPRECATED'}, + {'name': 'ALSONONDEPRECATED'}], + 'falseValues': [{'name': 'NONDEPRECATED'}, + {'name': 'ALSONONDEPRECATED'}], + 'omittedValues': [{'name': 'NONDEPRECATED'}, + {'name': 'ALSONONDEPRECATED'}], + }} + + +def test_fails_as_expected_on_the_type_root_field_without_an_arg(): + TestType = GraphQLObjectType('TestType', { + 'testField': GraphQLField(GraphQLString) + }) + schema = GraphQLSchema(TestType) + request = ''' + { + __type { + name + } + }''' + result = graphql(schema, request) + expected_error = {'message': ProvidedNonNullArguments.missing_field_arg_message('__type', 'name', 'String!'), + 'locations': [dict(line=3, column=9)]} + assert (expected_error in [format_error(error) for error in result.errors]) + + +def test_exposes_descriptions_on_types_and_fields(): + QueryRoot = GraphQLObjectType('QueryRoot', {'f': GraphQLField(GraphQLString)}) + schema = GraphQLSchema(QueryRoot) + request = '''{ + schemaType: __type(name: "__Schema") { + name, + description, + fields { + name, + description + } + } + } + ''' + result = graphql(schema, request) + assert not result.errors + assert result.data == {'schemaType': { + 'name': '__Schema', + 'description': 'A GraphQL Schema defines the capabilities of a ' + + 'GraphQL server. It exposes all available types and ' + + 'directives on the server, as well as the entry ' + + 'points for query, mutation and subscription operations.', + 'fields': [ + { + 'name': 'types', + 'description': 'A list of all types supported by this server.' + }, + { + 'name': 'queryType', + 'description': 'The type that query operations will be rooted at.' + }, + { + 'name': 'mutationType', + 'description': 'If this server supports mutation, the type that ' + 'mutation operations will be rooted at.' + }, + { + 'name': 'subscriptionType', + 'description': 'If this server support subscription, the type ' + 'that subscription operations will be rooted at.' + }, + { + 'name': 'directives', + 'description': 'A list of all directives supported by this server.' + } + ] + }} + + +def test_exposes_descriptions_on_enums(): + QueryRoot = GraphQLObjectType('QueryRoot', {'f': GraphQLField(GraphQLString)}) + schema = GraphQLSchema(QueryRoot) + request = '''{ + typeKindType: __type(name: "__TypeKind") { + name, + description, + enumValues { + name, + description + } + } + } + ''' + result = graphql(schema, request) + assert not result.errors + assert result.data == {'typeKindType': { + 'name': '__TypeKind', + 'description': 'An enum describing what kind of type a given `__Type` is', + 'enumValues': [ + { + 'description': 'Indicates this type is a scalar.', + 'name': 'SCALAR' + }, + { + 'description': 'Indicates this type is an object. ' + + '`fields` and `interfaces` are valid fields.', + 'name': 'OBJECT' + }, + { + 'description': 'Indicates this type is an interface. ' + + '`fields` and `possibleTypes` are valid fields.', + 'name': 'INTERFACE' + }, + { + 'description': 'Indicates this type is a union. ' + + '`possibleTypes` is a valid field.', + 'name': 'UNION' + }, + { + 'description': 'Indicates this type is an enum. ' + + '`enumValues` is a valid field.', + 'name': 'ENUM' + }, + { + 'description': 'Indicates this type is an input object. ' + + '`inputFields` is a valid field.', + 'name': 'INPUT_OBJECT' + }, + { + 'description': 'Indicates this type is a list. ' + + '`ofType` is a valid field.', + 'name': 'LIST' + }, + { + 'description': 'Indicates this type is a non-null. ' + + '`ofType` is a valid field.', + 'name': 'NON_NULL' + } + ] + }} diff --git a/tests/core_type/test_serialization.py b/graphql/type/tests/test_serialization.py similarity index 94% rename from tests/core_type/test_serialization.py rename to graphql/type/tests/test_serialization.py index 64d5881a..2c7d285d 100644 --- a/tests/core_type/test_serialization.py +++ b/graphql/type/tests/test_serialization.py @@ -1,5 +1,5 @@ -from graphql.core.type import (GraphQLBoolean, GraphQLFloat, GraphQLInt, - GraphQLString) +from graphql.type import (GraphQLBoolean, GraphQLFloat, GraphQLInt, + GraphQLString) def test_serializes_output_int(): diff --git a/tests/core_type/test_validation.py b/graphql/type/tests/test_validation.py similarity index 99% rename from tests/core_type/test_validation.py rename to graphql/type/tests/test_validation.py index 0efea487..1b2e2cbf 100644 --- a/tests/core_type/test_validation.py +++ b/graphql/type/tests/test_validation.py @@ -2,13 +2,12 @@ from pytest import raises -from graphql.core.type import (GraphQLEnumType, GraphQLEnumValue, GraphQLField, - GraphQLInputObjectField, GraphQLInputObjectType, - GraphQLInterfaceType, GraphQLList, - GraphQLNonNull, GraphQLObjectType, - GraphQLScalarType, GraphQLSchema, GraphQLString, - GraphQLUnionType) -from graphql.core.type.definition import GraphQLArgument +from graphql.type import (GraphQLEnumType, GraphQLEnumValue, GraphQLField, + GraphQLInputObjectField, GraphQLInputObjectType, + GraphQLInterfaceType, GraphQLList, GraphQLNonNull, + GraphQLObjectType, GraphQLScalarType, GraphQLSchema, + GraphQLString, GraphQLUnionType) +from graphql.type.definition import GraphQLArgument _none = lambda *args: None _true = lambda *args: True @@ -108,7 +107,8 @@ def schema_with_field_type(t): fields={ 'f': GraphQLField(t) } - ) + ), + types=[t] ) @@ -219,7 +219,7 @@ def test_it_rejects_a_schema_which_have_same_named_objects_implementing_an_inter ) with raises(AssertionError) as excinfo: - GraphQLSchema(query=QueryType) + GraphQLSchema(query=QueryType, types=[FirstBadObject, SecondBadObject]) assert str(excinfo.value) == 'Schema must contain unique named types but contains multiple types named ' \ '"BadObject".' diff --git a/tests/core_type/__init__.py b/graphql/utils/__init__.py similarity index 100% rename from tests/core_type/__init__.py rename to graphql/utils/__init__.py diff --git a/graphql/utils/assert_valid_name.py b/graphql/utils/assert_valid_name.py new file mode 100644 index 00000000..40afe596 --- /dev/null +++ b/graphql/utils/assert_valid_name.py @@ -0,0 +1,9 @@ +import re + +NAME_PATTERN = r'^[_a-zA-Z][_a-zA-Z0-9]*$' +COMPILED_NAME_PATTERN = re.compile(NAME_PATTERN) + + +def assert_valid_name(name): + '''Helper to assert that provided names are valid.''' + assert COMPILED_NAME_PATTERN.match(name), 'Names must match /{}/ but "{}" does not.'.format(NAME_PATTERN, name) diff --git a/graphql/core/utils/ast_from_value.py b/graphql/utils/ast_from_value.py similarity index 100% rename from graphql/core/utils/ast_from_value.py rename to graphql/utils/ast_from_value.py diff --git a/graphql/core/utils/ast_to_code.py b/graphql/utils/ast_to_code.py similarity index 100% rename from graphql/core/utils/ast_to_code.py rename to graphql/utils/ast_to_code.py diff --git a/graphql/core/utils/ast_to_dict.py b/graphql/utils/ast_to_dict.py similarity index 100% rename from graphql/core/utils/ast_to_dict.py rename to graphql/utils/ast_to_dict.py diff --git a/graphql/utils/base.py b/graphql/utils/base.py new file mode 100644 index 00000000..5e895853 --- /dev/null +++ b/graphql/utils/base.py @@ -0,0 +1,75 @@ +""" + Base GraphQL utilities + isort:skip_file +""" + +# The GraphQL query recommended for a full schema introspection. +from .introspection_query import introspection_query + +# Gets the target Operation from a Document +from .get_operation_ast import get_operation_ast + +# Build a GraphQLSchema from an introspection result. +from .build_client_schema import build_client_schema + +# Build a GraphQLSchema from a parsed GraphQL Schema language AST. +from .build_ast_schema import build_ast_schema + +# Extends an existing GraphQLSchema from a parsed GraphQL Schema language AST. +from .extend_schema import extend_schema + +# Print a GraphQLSchema to GraphQL Schema language. +from .schema_printer import print_schema, print_introspection_schema + +# Create a GraphQLType from a GraphQL language AST. +from .type_from_ast import type_from_ast + +# Create a JavaScript value from a GraphQL language AST. +from .value_from_ast import value_from_ast + +# Create a GraphQL language AST from a JavaScript value. +from .ast_from_value import ast_from_value + +# A helper to use within recursive-descent visitors which need to be aware of +# the GraphQL type system. +from .type_info import TypeInfo + +# Determine if JavaScript values adhere to a GraphQL type. +from .is_valid_value import is_valid_value + +# Determine if AST values adhere to a GraphQL type. +from .is_valid_literal_value import is_valid_literal_value + +# Concatenates multiple AST together. +from .concat_ast import concat_ast + +# Comparators for types +from .type_comparators import ( + is_equal_type, + is_type_sub_type_of, + do_types_overlap +) + +# Asserts that a string is a valid GraphQL name +from .assert_valid_name import assert_valid_name + +__all__ = [ + 'introspection_query', + 'get_operation_ast', + 'build_client_schema', + 'build_ast_schema', + 'extend_schema', + 'print_introspection_schema', + 'print_schema', + 'type_from_ast', + 'value_from_ast', + 'ast_from_value', + 'TypeInfo', + 'is_valid_value', + 'is_valid_literal_value', + 'concat_ast', + 'do_types_overlap', + 'is_equal_type', + 'is_type_sub_type_of', + 'assert_valid_name', +] diff --git a/graphql/utils/build_ast_schema.py b/graphql/utils/build_ast_schema.py new file mode 100644 index 00000000..3ed8f3a6 --- /dev/null +++ b/graphql/utils/build_ast_schema.py @@ -0,0 +1,260 @@ +from collections import OrderedDict + +from ..language import ast +from ..type import (GraphQLArgument, GraphQLBoolean, GraphQLDirective, + GraphQLEnumType, GraphQLEnumValue, GraphQLField, + GraphQLFloat, GraphQLID, GraphQLInputObjectField, + GraphQLInputObjectType, GraphQLInt, GraphQLInterfaceType, + GraphQLList, GraphQLNonNull, GraphQLObjectType, + GraphQLScalarType, GraphQLSchema, GraphQLString, + GraphQLUnionType) +from ..type.introspection import (__Directive, __DirectiveLocation, + __EnumValue, __Field, __InputValue, __Schema, + __Type, __TypeKind) +from ..utils.value_from_ast import value_from_ast + + +def _build_wrapped_type(inner_type, input_type_ast): + if isinstance(input_type_ast, ast.ListType): + return GraphQLList(_build_wrapped_type(inner_type, input_type_ast.type)) + + if isinstance(input_type_ast, ast.NonNullType): + return GraphQLNonNull(_build_wrapped_type(inner_type, input_type_ast.type)) + + return inner_type + + +def _get_inner_type_name(type_ast): + if isinstance(type_ast, (ast.ListType, ast.NonNullType)): + return _get_inner_type_name(type_ast.type) + + return type_ast.name.value + + +def _get_named_type_ast(type_ast): + named_type = type_ast + while isinstance(named_type, (ast.ListType, ast.NonNullType)): + named_type = named_type.type + + return named_type + + +def _false(*_): return False + + +def _none(*_): return None + + +def build_ast_schema(document): + assert isinstance(document, ast.Document), 'must pass in Document ast.' + + schema_def = None + + type_asts = ( + ast.ScalarTypeDefinition, + ast.ObjectTypeDefinition, + ast.InterfaceTypeDefinition, + ast.EnumTypeDefinition, + ast.UnionTypeDefinition, + ast.InputObjectTypeDefinition, + ) + + type_defs = [] + directive_defs = [] + + for d in document.definitions: + if isinstance(d, ast.SchemaDefinition): + if schema_def: + raise Exception('Must provide only one schema definition.') + schema_def = d + if isinstance(d, type_asts): + type_defs.append(d) + elif isinstance(d, ast.DirectiveDefinition): + directive_defs.append(d) + + if not schema_def: + raise Exception('Must provide a schema definition.') + + query_type_name = None + mutation_type_name = None + subscription_type_name = None + + for operation_type in schema_def.operation_types: + type_name = operation_type.type.name.value + if operation_type.operation == 'query': + if query_type_name: + raise Exception('Must provide only one query type in schema.') + query_type_name = type_name + elif operation_type.operation == 'mutation': + if mutation_type_name: + raise Exception('Must provide only one mutation type in schema.') + mutation_type_name = type_name + elif operation_type.operation == 'subscription': + if subscription_type_name: + raise Exception('Must provide only one subscription type in schema.') + subscription_type_name = type_name + + if not query_type_name: + raise Exception('Must provide schema definition with query type.') + + ast_map = {d.name.value: d for d in type_defs} + + if query_type_name not in ast_map: + raise Exception('Specified query type "{}" not found in document.'.format(query_type_name)) + + if mutation_type_name and mutation_type_name not in ast_map: + raise Exception('Specified mutation type "{}" not found in document.'.format(mutation_type_name)) + + if subscription_type_name and subscription_type_name not in ast_map: + raise Exception('Specified subscription type "{}" not found in document.'.format(subscription_type_name)) + + inner_type_map = OrderedDict([ + ('String', GraphQLString), + ('Int', GraphQLInt), + ('Float', GraphQLFloat), + ('Boolean', GraphQLBoolean), + ('ID', GraphQLID), + ('__Schema', __Schema), + ('__Directive', __Directive), + ('__DirectiveLocation', __DirectiveLocation), + ('__Type', __Type), + ('__Field', __Field), + ('__InputValue', __InputValue), + ('__EnumValue', __EnumValue), + ('__TypeKind', __TypeKind), + ]) + + def get_directive(directive_ast): + return GraphQLDirective( + name=directive_ast.name.value, + locations=[node.value for node in directive_ast.locations], + args=make_input_values(directive_ast.arguments, GraphQLArgument), + ) + + def get_object_type(type_ast): + type = type_def_named(type_ast.name.value) + assert isinstance(type, GraphQLObjectType), 'AST must provide object type' + return type + + def produce_type_def(type_ast): + type_name = _get_named_type_ast(type_ast).name.value + type_def = type_def_named(type_name) + return _build_wrapped_type(type_def, type_ast) + + def type_def_named(type_name): + if type_name in inner_type_map: + return inner_type_map[type_name] + + if type_name not in ast_map: + raise Exception('Type "{}" not found in document'.format(type_name)) + + inner_type_def = make_schema_def(ast_map[type_name]) + if not inner_type_def: + raise Exception('Nothing constructed for "{}".'.format(type_name)) + + inner_type_map[type_name] = inner_type_def + return inner_type_def + + def make_schema_def(definition): + if not definition: + raise Exception('def must be defined.') + + handler = _schema_def_handlers.get(type(definition)) + if not handler: + raise Exception('Type kind "{}" not supported.'.format(type(definition).__name__)) + + return handler(definition) + + def make_type_def(definition): + return GraphQLObjectType( + name=definition.name.value, + fields=lambda: make_field_def_map(definition), + interfaces=make_implemented_interfaces(definition) + ) + + def make_field_def_map(definition): + return OrderedDict( + (f.name.value, GraphQLField( + type=produce_type_def(f.type), + args=make_input_values(f.arguments, GraphQLArgument) + )) + for f in definition.fields + ) + + def make_implemented_interfaces(definition): + return [produce_type_def(i) for i in definition.interfaces] + + def make_input_values(values, cls): + return OrderedDict( + (value.name.value, cls( + type=produce_type_def(value.type), + default_value=value_from_ast(value.default_value, produce_type_def(value.type)) + )) + for value in values + ) + + def make_interface_def(definition): + return GraphQLInterfaceType( + name=definition.name.value, + resolve_type=_none, + fields=lambda: make_field_def_map(definition) + ) + + def make_enum_def(definition): + return GraphQLEnumType( + name=definition.name.value, + values=OrderedDict( + (v.name.value, GraphQLEnumValue()) for v in definition.values + ) + ) + + def make_union_def(definition): + return GraphQLUnionType( + name=definition.name.value, + resolve_type=_none, + types=[produce_type_def(t) for t in definition.types] + ) + + def make_scalar_def(definition): + return GraphQLScalarType( + name=definition.name.value, + serialize=_none, + # Validation calls the parse functions to determine if a literal value is correct. + # Returning none, however would cause the scalar to fail validation. Returning false, + # will cause them to pass. + parse_literal=_false, + parse_value=_false + ) + + def make_input_object_def(definition): + return GraphQLInputObjectType( + name=definition.name.value, + fields=make_input_values(definition.fields, GraphQLInputObjectField) + ) + + _schema_def_handlers = { + ast.ObjectTypeDefinition: make_type_def, + ast.InterfaceTypeDefinition: make_interface_def, + ast.EnumTypeDefinition: make_enum_def, + ast.UnionTypeDefinition: make_union_def, + ast.ScalarTypeDefinition: make_scalar_def, + ast.InputObjectTypeDefinition: make_input_object_def + } + types = [type_def_named(definition.name.value) for definition in type_defs] + directives = [get_directive(d) for d in directive_defs] + + schema_kwargs = {'query': get_object_type(ast_map[query_type_name])} + + if mutation_type_name: + schema_kwargs['mutation'] = get_object_type(ast_map[mutation_type_name]) + + if subscription_type_name: + schema_kwargs['subscription'] = get_object_type(ast_map[subscription_type_name]) + + if directive_defs: + schema_kwargs['directives'] = directives + + if types: + schema_kwargs['types'] = types + + return GraphQLSchema(**schema_kwargs) diff --git a/graphql/core/utils/build_client_schema.py b/graphql/utils/build_client_schema.py similarity index 72% rename from graphql/core/utils/build_client_schema.py rename to graphql/utils/build_client_schema.py index 649e0ca0..84ae9553 100644 --- a/graphql/core/utils/build_client_schema.py +++ b/graphql/utils/build_client_schema.py @@ -8,8 +8,10 @@ GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, GraphQLUnionType, is_input_type, is_output_type) -from ..type.directives import GraphQLDirective -from ..type.introspection import TypeKind +from ..type.directives import DirectiveLocation, GraphQLDirective +from ..type.introspection import (TypeKind, __Directive, __DirectiveLocation, + __EnumValue, __Field, __InputValue, __Schema, + __Type, __TypeKind) from .value_from_ast import value_from_ast @@ -33,7 +35,16 @@ def build_client_schema(introspection): 'Int': GraphQLInt, 'Float': GraphQLFloat, 'Boolean': GraphQLBoolean, - 'ID': GraphQLID + 'ID': GraphQLID, + '__Schema': __Schema, + '__Directive': __Directive, + '__DirectiveLocation': __DirectiveLocation, + '__Type': __Type, + '__Field': __Field, + '__InputValue': __InputValue, + '__EnumValue': __EnumValue, + '__TypeKind': __TypeKind, + } def get_type(type_ref): @@ -105,7 +116,7 @@ def build_type(type): def build_scalar_def(scalar_introspection): return GraphQLScalarType( name=scalar_introspection['name'], - description=scalar_introspection['description'], + description=scalar_introspection.get('description'), serialize=_none, parse_value=_false, parse_literal=_false @@ -114,15 +125,15 @@ def build_scalar_def(scalar_introspection): def build_object_def(object_introspection): return GraphQLObjectType( name=object_introspection['name'], - description=object_introspection['description'], - interfaces=[get_interface_type(i) for i in object_introspection['interfaces']], + description=object_introspection.get('description'), + interfaces=[get_interface_type(i) for i in object_introspection.get('interfaces', [])], fields=lambda: build_field_def_map(object_introspection) ) def build_interface_def(interface_introspection): return GraphQLInterfaceType( name=interface_introspection['name'], - description=interface_introspection['description'], + description=interface_introspection.get('description'), fields=lambda: build_field_def_map(interface_introspection), resolve_type=no_execution ) @@ -130,28 +141,28 @@ def build_interface_def(interface_introspection): def build_union_def(union_introspection): return GraphQLUnionType( name=union_introspection['name'], - description=union_introspection['description'], - types=[get_object_type(t) for t in union_introspection['possibleTypes']], + description=union_introspection.get('description'), + types=[get_object_type(t) for t in union_introspection.get('possibleTypes', [])], resolve_type=no_execution ) def build_enum_def(enum_introspection): return GraphQLEnumType( name=enum_introspection['name'], - description=enum_introspection['description'], + description=enum_introspection.get('description'), values=OrderedDict([(value_introspection['name'], - GraphQLEnumValue(description=value_introspection['description'], - deprecation_reason=value_introspection['deprecationReason'])) - for value_introspection in enum_introspection['enumValues'] + GraphQLEnumValue(description=value_introspection.get('description'), + deprecation_reason=value_introspection.get('deprecationReason'))) + for value_introspection in enum_introspection.get('enumValues', []) ]) ) def build_input_object_def(input_object_introspection): return GraphQLInputObjectType( name=input_object_introspection['name'], - description=input_object_introspection['description'], + description=input_object_introspection.get('description'), fields=lambda: build_input_value_def_map( - input_object_introspection['inputFields'], GraphQLInputObjectField + input_object_introspection.get('inputFields'), GraphQLInputObjectField ) ) @@ -168,11 +179,11 @@ def build_field_def_map(type_introspection): return OrderedDict([ (f['name'], GraphQLField( type=get_output_type(f['type']), - description=f['description'], + description=f.get('description'), resolver=no_execution, - deprecation_reason=f['deprecationReason'], - args=build_input_value_def_map(f['args'], GraphQLArgument))) - for f in type_introspection['fields'] + deprecation_reason=f.get('deprecationReason'), + args=build_input_value_def_map(f.get('args'), GraphQLArgument))) + for f in type_introspection.get('fields', []) ]) def build_default_value(f): @@ -197,17 +208,29 @@ def build_input_value(input_value_introspection, argument_type): return input_value def build_directive(directive_introspection): + # Support deprecated `on****` fields for building `locations`, as this + # is used by GraphiQL which may need to support outdated servers. + locations = list(directive_introspection.get('locations', [])) + if not locations: + locations = [] + if directive_introspection.get('onField', False): + locations += list(DirectiveLocation.FIELD_LOCATIONS) + if directive_introspection.get('onOperation', False): + locations += list(DirectiveLocation.OPERATION_LOCATIONS) + if directive_introspection.get('onFragment', False): + locations += list(DirectiveLocation.FRAGMENT_LOCATIONS) + return GraphQLDirective( name=directive_introspection['name'], - description=directive_introspection['description'], - args=[build_input_value(a, GraphQLArgument) for a in directive_introspection['args']], - on_operation=directive_introspection['onOperation'], - on_fragment=directive_introspection['onFragment'], - on_field=directive_introspection['onField'] + description=directive_introspection.get('description'), + # TODO: {} ? + args=build_input_value_def_map(directive_introspection.get('args', []), GraphQLArgument), + locations=locations ) - for type_introspection_name in type_introspection_map: - get_named_type(type_introspection_name) + # Iterate through all types, getting the type definition for each, ensuring + # that any type not directly referenced by a field will get created. + types = [get_named_type(type_introspection_name) for type_introspection_name in type_introspection_map.keys()] query_type = get_object_type(schema_introspection['queryType']) mutation_type = get_object_type( @@ -222,5 +245,6 @@ def build_directive(directive_introspection): query=query_type, mutation=mutation_type, subscription=subscription_type, - directives=directives + directives=directives, + types=types ) diff --git a/graphql/core/utils/concat_ast.py b/graphql/utils/concat_ast.py similarity index 100% rename from graphql/core/utils/concat_ast.py rename to graphql/utils/concat_ast.py diff --git a/graphql/core/utils/extend_schema.py b/graphql/utils/extend_schema.py similarity index 89% rename from graphql/core/utils/extend_schema.py rename to graphql/utils/extend_schema.py index c8aa42f2..28c4db06 100644 --- a/graphql/core/utils/extend_schema.py +++ b/graphql/utils/extend_schema.py @@ -8,6 +8,9 @@ GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLUnionType) +from ..type.introspection import (__Directive, __DirectiveLocation, + __EnumValue, __Field, __InputValue, __Schema, + __Type, __TypeKind) from ..type.scalars import (GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString) from ..type.schema import GraphQLSchema @@ -137,15 +140,15 @@ def extend_interface_type(type): name=type.name, description=type.description, fields=lambda: extend_field_map(type), - resolve_type=raise_client_schema_execution_error, + resolve_type=cannot_execute_client_schema, ) def extend_union_type(type): return GraphQLUnionType( name=type.name, description=type.description, - types=list(map(get_type_from_def, type.get_possible_types())), - resolve_type=raise_client_schema_execution_error, + types=list(map(get_type_from_def, type.get_types())), + resolve_type=cannot_execute_client_schema, ) def extend_implemented_interfaces(type): @@ -176,7 +179,7 @@ def extend_field_map(type): description=field.description, deprecation_reason=field.deprecation_reason, args={arg.name: arg for arg in field.args}, - resolver=raise_client_schema_execution_error, + resolver=cannot_execute_client_schema, ) # If there are any extensions to the fields, apply those here. @@ -194,7 +197,7 @@ def extend_field_map(type): new_field_map[field_name] = GraphQLField( build_field_type(field.type), args=build_input_values(field.arguments), - resolver=raise_client_schema_execution_error, + resolver=cannot_execute_client_schema, ) return new_field_map @@ -230,14 +233,14 @@ def build_interface_type(type_ast): return GraphQLInterfaceType( type_ast.name.value, fields=lambda: build_field_map(type_ast), - resolve_type=raise_client_schema_execution_error, + resolve_type=cannot_execute_client_schema, ) def build_union_type(type_ast): return GraphQLUnionType( type_ast.name.value, types=list(map(get_type_from_AST, type_ast.types)), - resolve_type=raise_client_schema_execution_error, + resolve_type=cannot_execute_client_schema, ) def build_scalar_type(type_ast): @@ -273,7 +276,7 @@ def build_field_map(type_ast): field.name.value: GraphQLField( build_field_type(field.type), args=build_input_values(field.arguments), - resolver=raise_client_schema_execution_error, + resolver=cannot_execute_client_schema, ) for field in type_ast.fields } @@ -300,14 +303,24 @@ def build_field_type(type_ast): return schema # A cache to use to store the actual GraphQLType definition objects by name. - # Initialize to the GraphQL built in scalars. All functions below are inline - # so that this type def cache is within the scope of the closure. + # Initialize to the GraphQL built in scalars and introspection types. All + # functions below are inline so that this type def cache is within the scope + # of the closure. + type_def_cache = { 'String': GraphQLString, 'Int': GraphQLInt, 'Float': GraphQLFloat, 'Boolean': GraphQLBoolean, 'ID': GraphQLID, + '__Schema': __Schema, + '__Directive': __Directive, + '__DirectiveLocation': __DirectiveLocation, + '__Type': __Type, + '__Field': __Field, + '__InputValue': __InputValue, + '__EnumValue': __EnumValue, + '__TypeKind': __TypeKind, } # Get the root Query, Mutation, and Subscription types. @@ -323,12 +336,10 @@ def build_field_type(type_ast): # Iterate through all types, getting the type definition for each, ensuring # that any type not directly referenced by a field will get created. - for typeName, _def in schema.get_type_map().items(): - get_type_from_def(_def) + types = [get_type_from_def(_def) for _def in schema.get_type_map().values()] - # Do the same with new types. - for typeName, _def in type_definition_map.items(): - get_type_from_AST(_def) + # Do the same with new types, appending to the list of defined types. + types += [get_type_from_AST(_def) for _def in type_definition_map.values()] # Then produce and return a Schema with these types. return GraphQLSchema( @@ -337,8 +348,9 @@ def build_field_type(type_ast): subscription=subscription_type, # Copy directives. directives=schema.get_directives(), + types=types ) -def raise_client_schema_execution_error(*args, **kwargs): +def cannot_execute_client_schema(*args, **kwargs): raise Exception('Client Schema cannot be used for execution.') diff --git a/graphql/core/utils/get_field_def.py b/graphql/utils/get_field_def.py similarity index 100% rename from graphql/core/utils/get_field_def.py rename to graphql/utils/get_field_def.py diff --git a/graphql/core/utils/get_operation_ast.py b/graphql/utils/get_operation_ast.py similarity index 96% rename from graphql/core/utils/get_operation_ast.py rename to graphql/utils/get_operation_ast.py index ae5f4a1a..899e907c 100644 --- a/graphql/core/utils/get_operation_ast.py +++ b/graphql/utils/get_operation_ast.py @@ -1,4 +1,4 @@ -from ...core.language import ast +from ..language import ast def get_operation_ast(document_ast, operation_name=None): diff --git a/graphql/core/utils/introspection_query.py b/graphql/utils/introspection_query.py similarity index 94% rename from graphql/core/utils/introspection_query.py rename to graphql/utils/introspection_query.py index 0a7a2219..5b08636b 100644 --- a/graphql/core/utils/introspection_query.py +++ b/graphql/utils/introspection_query.py @@ -10,12 +10,10 @@ directives { name description + locations args { ...InputValue } - onOperation - onFragment - onField } } } diff --git a/graphql/core/utils/is_valid_literal_value.py b/graphql/utils/is_valid_literal_value.py similarity index 100% rename from graphql/core/utils/is_valid_literal_value.py rename to graphql/utils/is_valid_literal_value.py diff --git a/graphql/core/utils/is_valid_value.py b/graphql/utils/is_valid_value.py similarity index 97% rename from graphql/core/utils/is_valid_value.py rename to graphql/utils/is_valid_value.py index 07c02553..6cabb0b4 100644 --- a/graphql/core/utils/is_valid_value.py +++ b/graphql/utils/is_valid_value.py @@ -1,5 +1,5 @@ """ - Implementation of isValidJSValue from graphql-js + Implementation of isValidJSValue from graphql.s """ import collections diff --git a/graphql/core/utils/schema_printer.py b/graphql/utils/schema_printer.py similarity index 67% rename from graphql/core/utils/schema_printer.py rename to graphql/utils/schema_printer.py index a6b22702..37fe33ed 100644 --- a/graphql/core/utils/schema_printer.py +++ b/graphql/utils/schema_printer.py @@ -6,11 +6,15 @@ def print_schema(schema): - return _print_filtered_schema(schema, _is_defined_type) + return _print_filtered_schema(schema, lambda n: not(is_spec_directive(n)), _is_defined_type) def print_introspection_schema(schema): - return _print_filtered_schema(schema, _is_introspection_type) + return _print_filtered_schema(schema, is_spec_directive, _is_introspection_type) + + +def is_spec_directive(directive_name): + return directive_name in ('skip', 'include') def _is_defined_type(typename): @@ -28,12 +32,36 @@ def _is_builtin_scalar(typename): return typename in _builtin_scalars -def _print_filtered_schema(schema, type_filter): - return '\n\n'.join( +def _print_filtered_schema(schema, directive_filter, type_filter): + return '\n\n'.join([ + _print_schema_definition(schema) + ] + [ + _print_directive(directive) + for directive in schema.get_directives() + if directive_filter(directive.name) + ] + [ _print_type(type) for typename, type in sorted(schema.get_type_map().items()) if type_filter(typename) - ) + '\n' + ]) + '\n' + + +def _print_schema_definition(schema): + operation_types = [] + + query_type = schema.get_query_type() + if query_type: + operation_types.append(' query: {}'.format(query_type)) + + mutation_type = schema.get_mutation_type() + if mutation_type: + operation_types.append(' mutation: {}'.format(mutation_type)) + + subscription_type = schema.get_subscription_type() + if subscription_type: + operation_types.append(' subscription: {}'.format(subscription_type)) + + return 'schema {{\n{}\n}}'.format('\n'.join(operation_types)) def _print_type(type): @@ -81,7 +109,7 @@ def _print_interface(type): def _print_union(type): - return 'union {} = {}'.format(type.name, ' | '.join(str(t) for t in type.get_possible_types())) + return 'union {} = {}'.format(type.name, ' | '.join(str(t) for t in type.get_types())) def _print_enum(type): @@ -104,11 +132,11 @@ def _print_fields(type): return '\n'.join(' {}{}: {}'.format(f.name, _print_args(f), f.type) for f in type.get_fields().values()) -def _print_args(field): - if not field.args: +def _print_args(field_or_directives): + if not field_or_directives.args: return '' - return '({})'.format(', '.join(_print_input_value(arg) for arg in field.args)) + return '({})'.format(', '.join(_print_input_value(arg) for arg in field_or_directives.args)) def _print_input_value(arg): @@ -120,4 +148,8 @@ def _print_input_value(arg): return '{}: {}{}'.format(arg.name, arg.type, default_value) +def _print_directive(directive): + return 'directive @{}{} on {}'.format(directive.name, _print_args(directive), ' | '.join(directive.locations)) + + __all__ = ['print_schema', 'print_introspection_schema'] diff --git a/tests/core_utils/__init__.py b/graphql/utils/tests/__init__.py similarity index 100% rename from tests/core_utils/__init__.py rename to graphql/utils/tests/__init__.py diff --git a/tests/core_utils/test_ast_from_value.py b/graphql/utils/tests/test_ast_from_value.py similarity index 89% rename from tests/core_utils/test_ast_from_value.py rename to graphql/utils/tests/test_ast_from_value.py index dd7deca4..5ac23072 100644 --- a/tests/core_utils/test_ast_from_value.py +++ b/graphql/utils/tests/test_ast_from_value.py @@ -1,11 +1,11 @@ from collections import OrderedDict -from graphql.core.language import ast -from graphql.core.type.definition import (GraphQLEnumType, GraphQLEnumValue, - GraphQLInputObjectField, - GraphQLInputObjectType, GraphQLList) -from graphql.core.type.scalars import GraphQLFloat -from graphql.core.utils.ast_from_value import ast_from_value +from graphql.language import ast +from graphql.type.definition import (GraphQLEnumType, GraphQLEnumValue, + GraphQLInputObjectField, + GraphQLInputObjectType, GraphQLList) +from graphql.type.scalars import GraphQLFloat +from graphql.utils.ast_from_value import ast_from_value def test_converts_boolean_values_to_asts(): diff --git a/tests/core_utils/test_ast_to_code.py b/graphql/utils/tests/test_ast_to_code.py similarity index 59% rename from tests/core_utils/test_ast_to_code.py rename to graphql/utils/tests/test_ast_to_code.py index 37ea399c..98de219f 100644 --- a/tests/core_utils/test_ast_to_code.py +++ b/graphql/utils/tests/test_ast_to_code.py @@ -1,8 +1,9 @@ -from graphql.core import Source, parse -from graphql.core.language import ast -from graphql.core.language.parser import Loc -from graphql.core.utils.ast_to_code import ast_to_code -from tests.core_language import fixtures +from graphql import Source, parse +from graphql.language import ast +from graphql.language.parser import Loc +from graphql.utils.ast_to_code import ast_to_code + +from ...language.tests import fixtures def test_ast_to_code_using_kitchen_sink(): diff --git a/tests/core_utils/test_ast_to_dict.py b/graphql/utils/tests/test_ast_to_dict.py similarity index 98% rename from tests/core_utils/test_ast_to_dict.py rename to graphql/utils/tests/test_ast_to_dict.py index ced91240..6c4cdc49 100644 --- a/tests/core_utils/test_ast_to_dict.py +++ b/graphql/utils/tests/test_ast_to_dict.py @@ -1,6 +1,6 @@ -from graphql.core.language import ast -from graphql.core.language.parser import Loc, parse -from graphql.core.utils.ast_to_dict import ast_to_dict +from graphql.language import ast +from graphql.language.parser import Loc, parse +from graphql.utils.ast_to_dict import ast_to_dict def test_converts_simple_ast_to_dict(): diff --git a/graphql/utils/tests/test_build_ast_schema.py b/graphql/utils/tests/test_build_ast_schema.py new file mode 100644 index 00000000..5cb45ccd --- /dev/null +++ b/graphql/utils/tests/test_build_ast_schema.py @@ -0,0 +1,619 @@ +from pytest import raises + +from graphql import parse +from graphql.utils.build_ast_schema import build_ast_schema +from graphql.utils.schema_printer import print_schema + + +def cycle_output(body): + ast = parse(body) + schema = build_ast_schema(ast) + return '\n' + print_schema(schema) + + +def test_simple_type(): + body = ''' +schema { + query: HelloScalars +} + +type HelloScalars { + str: String + int: Int + float: Float + id: ID + bool: Boolean +} +''' + output = cycle_output(body) + assert output == body + + +def test_with_directives(): + body = ''' +schema { + query: Hello +} + +directive @foo(arg: Int) on FIELD + +type Hello { + str: String +} +''' + output = cycle_output(body) + assert output == body + + +def test_type_modifiers(): + body = ''' +schema { + query: HelloScalars +} + +type HelloScalars { + nonNullStr: String! + listOfStrs: [String] + listOfNonNullStrs: [String!] + nonNullListOfStrs: [String]! + nonNullListOfNonNullStrs: [String!]! +} +''' + output = cycle_output(body) + assert output == body + + +def test_recursive_type(): + body = ''' +schema { + query: Recurse +} + +type Recurse { + str: String + recurse: Recurse +} +''' + output = cycle_output(body) + assert output == body + + +def test_two_types_circular(): + body = ''' +schema { + query: TypeOne +} + +type TypeOne { + str: String + typeTwo: TypeTwo +} + +type TypeTwo { + str: String + typeOne: TypeOne +} +''' + output = cycle_output(body) + assert output == body + + +def test_single_argument_field(): + body = ''' +schema { + query: Hello +} + +type Hello { + str(int: Int): String + floatToStr(float: Float): String + idToStr(id: ID): String + booleanToStr(bool: Boolean): String + strToStr(bool: String): String +} +''' + output = cycle_output(body) + assert output == body + + +def test_simple_type_with_multiple_arguments(): + body = ''' +schema { + query: Hello +} + +type Hello { + str(int: Int, bool: Boolean): String +} +''' + output = cycle_output(body) + assert output == body + + +def test_simple_type_with_interface(): + body = ''' +schema { + query: HelloInterface +} + +type HelloInterface implements WorldInterface { + str: String +} + +interface WorldInterface { + str: String +} +''' + output = cycle_output(body) + assert output == body + + +def test_simple_output_enum(): + body = ''' +schema { + query: OutputEnumRoot +} + +enum Hello { + WORLD +} + +type OutputEnumRoot { + hello: Hello +} +''' + output = cycle_output(body) + assert output == body + + +def test_simple_input_enum(): + body = ''' +schema { + query: InputEnumRoot +} + +enum Hello { + WORLD +} + +type InputEnumRoot { + str(hello: Hello): String +} +''' + output = cycle_output(body) + assert output == body + + +def test_multiple_value_enum(): + body = ''' +schema { + query: OutputEnumRoot +} + +enum Hello { + WO + RLD +} + +type OutputEnumRoot { + hello: Hello +} +''' + output = cycle_output(body) + assert output == body + + +def test_simple_union(): + body = ''' +schema { + query: Root +} + +union Hello = World + +type Root { + hello: Hello +} + +type World { + str: String +} +''' + output = cycle_output(body) + assert output == body + + +def test_multiple_union(): + body = ''' +schema { + query: Root +} + +union Hello = WorldOne | WorldTwo + +type Root { + hello: Hello +} + +type WorldOne { + str: String +} + +type WorldTwo { + str: String +} +''' + output = cycle_output(body) + assert output == body + + +def test_custom_scalar(): + body = ''' +schema { + query: Root +} + +scalar CustomScalar + +type Root { + customScalar: CustomScalar +} +''' + output = cycle_output(body) + assert output == body + + +def test_input_object(): + body = ''' +schema { + query: Root +} + +input Input { + int: Int +} + +type Root { + field(in: Input): String +} +''' + output = cycle_output(body) + assert output == body + + +def test_simple_argument_field_with_default(): + body = ''' +schema { + query: Hello +} + +type Hello { + str(int: Int = 2): String +} +''' + output = cycle_output(body) + assert output == body + + +def test_simple_type_with_mutation(): + body = ''' +schema { + query: HelloScalars + mutation: Mutation +} + +type HelloScalars { + str: String + int: Int + bool: Boolean +} + +type Mutation { + addHelloScalars(str: String, int: Int, bool: Boolean): HelloScalars +} +''' + output = cycle_output(body) + assert output == body + + +def test_simple_type_with_subscription(): + body = ''' +schema { + query: HelloScalars + subscription: Subscription +} + +type HelloScalars { + str: String + int: Int + bool: Boolean +} + +type Subscription { + subscribeHelloScalars(str: String, int: Int, bool: Boolean): HelloScalars +} +''' + output = cycle_output(body) + assert output == body + + +def test_unreferenced_type_implementing_referenced_interface(): + body = ''' +schema { + query: Query +} + +type Concrete implements Iface { + key: String +} + +interface Iface { + key: String +} + +type Query { + iface: Iface +} +''' + output = cycle_output(body) + assert output == body + + +def test_unreferenced_type_implementing_referenced_union(): + body = ''' +schema { + query: Query +} + +type Concrete { + key: String +} + +type Query { + union: Union +} + +union Union = Concrete +''' + output = cycle_output(body) + assert output == body + + +def test_requires_a_schema_definition(): + body = ''' +type Hello { + bar: Bar +} +''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Must provide a schema definition.' == str(excinfo.value) + + +def test_allows_only_a_single_schema_definition(): + body = ''' +schema { + query: Hello +} + +schema { + query: Hello +} + +type Hello { + bar: Bar +} +''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Must provide only one schema definition.' == str(excinfo.value) + + +def test_requires_a_query_type(): + body = ''' +schema { + mutation: Hello +} + +type Hello { + bar: Bar +} +''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Must provide schema definition with query type.' == str(excinfo.value) + + +def test_allows_only_a_single_query_type(): + body = ''' +schema { + query: Hello + query: Yellow +} + +type Hello { + bar: Bar +} + +type Yellow { + isColor: Boolean +} +''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Must provide only one query type in schema.' == str(excinfo.value) + + +def test_allows_only_a_single_mutation_type(): + body = ''' +schema { + query: Hello + mutation: Hello + mutation: Yellow +} + +type Hello { + bar: Bar +} + +type Yellow { + isColor: Boolean +} +''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Must provide only one mutation type in schema.' == str(excinfo.value) + + +def test_allows_only_a_single_subscription_type(): + body = ''' +schema { + query: Hello + subscription: Hello + subscription: Yellow +} + +type Hello { + bar: Bar +} + +type Yellow { + isColor: Boolean +} +''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Must provide only one subscription type in schema.' == str(excinfo.value) + + +def test_unknown_type_referenced(): + body = ''' +schema { + query: Hello +} + +type Hello { + bar: Bar +} +''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Type "Bar" not found in document' in str(excinfo.value) + + +def test_unknown_type_in_union_list(): + body = ''' +schema { + query: Hello +} + +union TestUnion = Bar +type Hello { testUnion: TestUnion } +''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Type "Bar" not found in document' in str(excinfo.value) + + +def test_unknown_query_type(): + body = ''' +schema { + query: Wat +} + +type Hello { + str: String +} +''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Specified query type "Wat" not found in document' in str(excinfo.value) + + +def test_unknown_mutation_type(): + body = ''' +schema { + query: Hello + mutation: Wat +} + +type Hello { + str: String +} +''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Specified mutation type "Wat" not found in document' in str(excinfo.value) + + +def test_unknown_subscription_type(): + body = ''' +schema { + query: Hello + mutation: Wat + subscription: Awesome +} + +type Hello { + str: String +} + +type Wat { + str: String +} +''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Specified subscription type "Awesome" not found in document' in str(excinfo.value) + + +def test_does_not_consider_query_names(): + body = ''' +schema { + query: Foo +} + +type Hello { + str: String +} +''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Specified query type "Foo" not found in document' in str(excinfo.value) + + +def test_does_not_consider_fragment_names(): + body = '''schema { + query: Foo +} + +fragment Foo on Type { field } ''' + doc = parse(body) + with raises(Exception) as excinfo: + build_ast_schema(doc) + + assert 'Specified query type "Foo" not found in document' in str(excinfo.value) diff --git a/tests/core_utils/test_build_client_schema.py b/graphql/utils/tests/test_build_client_schema.py similarity index 81% rename from tests/core_utils/test_build_client_schema.py rename to graphql/utils/tests/test_build_client_schema.py index 9b9dc9c8..21ea1c11 100644 --- a/tests/core_utils/test_build_client_schema.py +++ b/graphql/utils/tests/test_build_client_schema.py @@ -2,26 +2,27 @@ from pytest import raises -from graphql.core import graphql -from graphql.core.error import format_error -from graphql.core.type import (GraphQLArgument, GraphQLBoolean, - GraphQLEnumType, GraphQLEnumValue, GraphQLField, - GraphQLFloat, GraphQLID, - GraphQLInputObjectField, GraphQLInputObjectType, - GraphQLInt, GraphQLInterfaceType, GraphQLList, - GraphQLNonNull, GraphQLObjectType, - GraphQLScalarType, GraphQLSchema, GraphQLString, - GraphQLUnionType) -from graphql.core.type.directives import GraphQLDirective -from graphql.core.utils.build_client_schema import build_client_schema -from graphql.core.utils.introspection_query import introspection_query +from graphql import graphql +from graphql.error import format_error +from graphql.type import (GraphQLArgument, GraphQLBoolean, GraphQLEnumType, + GraphQLEnumValue, GraphQLField, GraphQLFloat, + GraphQLID, GraphQLInputObjectField, + GraphQLInputObjectType, GraphQLInt, + GraphQLInterfaceType, GraphQLList, GraphQLNonNull, + GraphQLObjectType, GraphQLScalarType, GraphQLSchema, + GraphQLString, GraphQLUnionType) +from graphql.type.directives import GraphQLDirective +from graphql.utils.build_client_schema import build_client_schema +from graphql.utils.introspection_query import introspection_query + +from ...pyutils.contain_subset import contain_subset def _test_schema(server_schema): initial_introspection = graphql(server_schema, introspection_query) client_schema = build_client_schema(initial_introspection.data) second_introspection = graphql(client_schema, introspection_query) - assert initial_introspection.data == second_introspection.data + assert contain_subset(initial_introspection.data, second_introspection.data) return client_schema @@ -149,7 +150,15 @@ def test_builds_a_schema_with_an_interface(): } ) - GraphQLObjectType( + DogType = GraphQLObjectType( + name='DogType', + interfaces=[FriendlyType], + fields=lambda: { + 'bestFriend': GraphQLField(FriendlyType) + } + ) + + HumanType = GraphQLObjectType( name='Human', interfaces=[FriendlyType], fields=lambda: { @@ -163,6 +172,36 @@ def test_builds_a_schema_with_an_interface(): fields={ 'friendly': GraphQLField(FriendlyType) } + ), + types=[DogType, HumanType] + ) + + _test_schema(schema) + + +def test_builds_a_schema_with_an_implicit_interface(): + FriendlyType = GraphQLInterfaceType( + name='Friendly', + resolve_type=lambda: None, + fields=lambda: { + 'bestFriend': GraphQLField(FriendlyType, description='The best friend of this friendly thing.') + } + ) + + DogType = GraphQLObjectType( + name='DogType', + interfaces=[FriendlyType], + fields=lambda: { + 'bestFriend': GraphQLField(DogType) + } + ) + + schema = GraphQLSchema( + query=GraphQLObjectType( + name='WithInterface', + fields={ + 'dog': GraphQLField(DogType) + } ) ) @@ -389,7 +428,7 @@ def test_builds_a_schema_with_custom_directives(): GraphQLDirective( name='customDirective', description='This is a custom directive', - on_field=True + locations=['FIELD'] ) ] ) @@ -397,6 +436,73 @@ def test_builds_a_schema_with_custom_directives(): _test_schema(schema) +def test_builds_a_schema_with_legacy_directives(): + old_introspection = { + "__schema": { + "queryType": { + "name": "Simple" + }, + "types": [{ + "name": "Simple", + "kind": "OBJECT", + "fields": [{ + "name": "simple", + "args": [], + "type": { + "name": "Simple" + } + }], + "interfaces": [] + }], + "directives": [{ + "name": "Old1", + "args": [], + "onField": True + }, { + "name": "Old2", + "args": [], + "onFragment": True + }, { + "name": "Old3", + "args": [], + "onOperation": True + }, { + "name": "Old4", + "args": [], + "onField": True, + "onFragment": True + }] + } + } + + new_introspection = { + "__schema": { + "directives": [{ + "name": "Old1", + "args": [], + "locations": ["FIELD"] + }, { + "name": "Old2", + "args": [], + "locations": ["FRAGMENT_DEFINITION", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"] + }, { + "name": "Old3", + "args": [], + "locations": ["QUERY", "MUTATION", "SUBSCRIPTION"] + }, { + "name": "Old4", + "args": [], + "locations": ["FIELD", "FRAGMENT_DEFINITION", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"] + }] + } + } + + client_schema = build_client_schema(old_introspection) + second_introspection = graphql(client_schema, introspection_query).data + + assert contain_subset(new_introspection, second_introspection) + + def test_builds_a_schema_aware_of_deprecation(): schema = GraphQLSchema( query=GraphQLObjectType( diff --git a/tests/core_utils/test_concat_ast.py b/graphql/utils/tests/test_concat_ast.py similarity index 71% rename from tests/core_utils/test_concat_ast.py rename to graphql/utils/tests/test_concat_ast.py index 34cfb8f6..5218b298 100644 --- a/tests/core_utils/test_concat_ast.py +++ b/graphql/utils/tests/test_concat_ast.py @@ -1,6 +1,6 @@ -from graphql.core import Source, parse -from graphql.core.language.printer import print_ast -from graphql.core.utils.concat_ast import concat_ast +from graphql import Source, parse +from graphql.language.printer import print_ast +from graphql.utils.concat_ast import concat_ast def test_it_concatenates_two_acts_together(): diff --git a/tests/core_utils/test_extend_schema.py b/graphql/utils/tests/test_extend_schema.py similarity index 77% rename from tests/core_utils/test_extend_schema.py rename to graphql/utils/tests/test_extend_schema.py index c5db47ce..194d984d 100644 --- a/tests/core_utils/test_extend_schema.py +++ b/graphql/utils/tests/test_extend_schema.py @@ -2,14 +2,14 @@ from pytest import raises -from graphql.core import parse -from graphql.core.execution import execute -from graphql.core.type import (GraphQLArgument, GraphQLField, GraphQLID, - GraphQLInterfaceType, GraphQLList, - GraphQLNonNull, GraphQLObjectType, - GraphQLSchema, GraphQLString, GraphQLUnionType) -from graphql.core.utils.extend_schema import extend_schema -from graphql.core.utils.schema_printer import print_schema +from graphql import parse +from graphql.execution import execute +from graphql.type import (GraphQLArgument, GraphQLEnumType, GraphQLEnumValue, + GraphQLField, GraphQLID, GraphQLInterfaceType, + GraphQLList, GraphQLNonNull, GraphQLObjectType, + GraphQLSchema, GraphQLString, GraphQLUnionType) +from graphql.utils.extend_schema import extend_schema +from graphql.utils.schema_printer import print_schema # Test schema. SomeInterfaceType = GraphQLInterfaceType( @@ -55,12 +55,21 @@ types=[FooType, BizType], ) +SomeEnumType = GraphQLEnumType( + name='SomeEnum', + values=OrderedDict([ + ('ONE', GraphQLEnumValue(1)), + ('TWO', GraphQLEnumValue(2)), + ]) +) + test_schema = GraphQLSchema( query=GraphQLObjectType( name='Query', fields=lambda: OrderedDict([ ('foo', GraphQLField(FooType)), ('someUnion', GraphQLField(SomeUnionType)), + ('someEnum', GraphQLField(SomeEnumType)), ('someInterface', GraphQLField( SomeInterfaceType, args={ @@ -68,7 +77,8 @@ }, )), ]) - ) + ), + types=[FooType, BarType] ) @@ -101,7 +111,7 @@ def test_cannot_be_used_for_execution(): extended_schema = extend_schema(test_schema, ast) clientQuery = parse('{ newField }') - result = execute(extended_schema, object(), clientQuery) + result = execute(extended_schema, clientQuery, object()) assert result.data['newField'] is None assert str(result.errors[0] ) == 'Client Schema cannot be used for execution.' @@ -119,7 +129,11 @@ def test_extends_objects_by_adding_new_fields(): assert print_schema(test_schema) == original_print # print original_print assert print_schema(extended_schema) == \ - '''type Bar implements SomeInterface { + '''schema { + query: Query +} + +type Bar implements SomeInterface { name: String some: SomeInterface foo: Foo @@ -139,15 +153,78 @@ def test_extends_objects_by_adding_new_fields(): type Query { foo: Foo someUnion: SomeUnion + someEnum: SomeEnum + someInterface(id: ID!): SomeInterface +} + +enum SomeEnum { + ONE + TWO +} + +interface SomeInterface { + name: String + some: SomeInterface +} + +union SomeUnion = Foo | Biz +''' + + +def test_extends_objects_by_adding_new_unused_types(): + ast = parse(''' + type Unused { + someField: String + } + ''') + original_print = print_schema(test_schema) + extended_schema = extend_schema(test_schema, ast) + assert extended_schema != test_schema + assert print_schema(test_schema) == original_print + # print original_print + assert print_schema(extended_schema) == \ + '''schema { + query: Query +} + +type Bar implements SomeInterface { + name: String + some: SomeInterface + foo: Foo +} + +type Biz { + fizz: String +} + +type Foo implements SomeInterface { + name: String + some: SomeInterface + tree: [Foo]! +} + +type Query { + foo: Foo + someUnion: SomeUnion + someEnum: SomeEnum someInterface(id: ID!): SomeInterface } +enum SomeEnum { + ONE + TWO +} + interface SomeInterface { name: String some: SomeInterface } union SomeUnion = Foo | Biz + +type Unused { + someField: String +} ''' @@ -167,7 +244,11 @@ def test_extends_objects_by_adding_new_fields_with_arguments(): assert extended_schema != test_schema assert print_schema(test_schema) == original_print assert print_schema(extended_schema) == \ - '''type Bar implements SomeInterface { + '''schema { + query: Query +} + +type Bar implements SomeInterface { name: String some: SomeInterface foo: Foo @@ -193,9 +274,68 @@ def test_extends_objects_by_adding_new_fields_with_arguments(): type Query { foo: Foo someUnion: SomeUnion + someEnum: SomeEnum someInterface(id: ID!): SomeInterface } +enum SomeEnum { + ONE + TWO +} + +interface SomeInterface { + name: String + some: SomeInterface +} + +union SomeUnion = Foo | Biz +''' + + +def test_extends_objects_by_adding_new_fields_with_existing_types(): + ast = parse(''' + extend type Foo { + newField(arg1: SomeEnum!): SomeEnum + } + ''') + original_print = print_schema(test_schema) + extended_schema = extend_schema(test_schema, ast) + assert extended_schema != test_schema + assert print_schema(test_schema) == original_print + assert print_schema(extended_schema) == \ + '''schema { + query: Query +} + +type Bar implements SomeInterface { + name: String + some: SomeInterface + foo: Foo +} + +type Biz { + fizz: String +} + +type Foo implements SomeInterface { + name: String + some: SomeInterface + tree: [Foo]! + newField(arg1: SomeEnum!): SomeEnum +} + +type Query { + foo: Foo + someUnion: SomeUnion + someEnum: SomeEnum + someInterface(id: ID!): SomeInterface +} + +enum SomeEnum { + ONE + TWO +} + interface SomeInterface { name: String some: SomeInterface @@ -217,7 +357,11 @@ def test_extends_objects_by_adding_implemented_interfaces(): assert extended_schema != test_schema assert print_schema(test_schema) == original_print assert print_schema(extended_schema) == \ - '''type Bar implements SomeInterface { + '''schema { + query: Query +} + +type Bar implements SomeInterface { name: String some: SomeInterface foo: Foo @@ -238,9 +382,15 @@ def test_extends_objects_by_adding_implemented_interfaces(): type Query { foo: Foo someUnion: SomeUnion + someEnum: SomeEnum someInterface(id: ID!): SomeInterface } +enum SomeEnum { + ONE + TWO +} + interface SomeInterface { name: String some: SomeInterface @@ -250,7 +400,7 @@ def test_extends_objects_by_adding_implemented_interfaces(): ''' -def test_extends_objects_by_adding_implemented_interfaces(): +def test_extends_objects_by_adding_implemented_interfaces_2(): ast = parse(''' extend type Foo { newObject: NewObject @@ -281,7 +431,11 @@ def test_extends_objects_by_adding_implemented_interfaces(): assert extended_schema != test_schema assert print_schema(test_schema) == original_print assert print_schema(extended_schema) == \ - '''type Bar implements SomeInterface { + '''schema { + query: Query +} + +type Bar implements SomeInterface { name: String some: SomeInterface foo: Foo @@ -327,9 +481,15 @@ def test_extends_objects_by_adding_implemented_interfaces(): type Query { foo: Foo someUnion: SomeUnion + someEnum: SomeEnum someInterface(id: ID!): SomeInterface } +enum SomeEnum { + ONE + TWO +} + interface SomeInterface { name: String some: SomeInterface @@ -353,7 +513,11 @@ def test_extends_objects_by_adding_implemented_new_interfaces(): assert extended_schema != test_schema assert print_schema(test_schema) == original_print assert print_schema(extended_schema) == \ - '''type Bar implements SomeInterface { + '''schema { + query: Query +} + +type Bar implements SomeInterface { name: String some: SomeInterface foo: Foo @@ -377,9 +541,15 @@ def test_extends_objects_by_adding_implemented_new_interfaces(): type Query { foo: Foo someUnion: SomeUnion + someEnum: SomeEnum someInterface(id: ID!): SomeInterface } +enum SomeEnum { + ONE + TWO +} + interface SomeInterface { name: String some: SomeInterface @@ -412,7 +582,11 @@ def test_extends_objects_multiple_times(): assert extended_schema != test_schema assert print_schema(test_schema) == original_print assert print_schema(extended_schema) == \ - '''type Bar implements SomeInterface { + '''schema { + query: Query +} + +type Bar implements SomeInterface { name: String some: SomeInterface foo: Foo @@ -440,9 +614,15 @@ def test_extends_objects_multiple_times(): type Query { foo: Foo someUnion: SomeUnion + someEnum: SomeEnum someInterface(id: ID!): SomeInterface } +enum SomeEnum { + ONE + TWO +} + interface SomeInterface { name: String some: SomeInterface @@ -490,7 +670,13 @@ def test_may_extend_mutations_and_subscriptions(): assert extended_schema != mutationSchema assert print_schema(mutationSchema) == original_print assert print_schema(extended_schema) == \ - '''type Mutation { + '''schema { + query: Query + mutation: Mutation + subscription: Subscription +} + +type Mutation { mutationField: String newMutationField: Int } diff --git a/tests/core_utils/test_get_operation_ast.py b/graphql/utils/tests/test_get_operation_ast.py similarity index 95% rename from tests/core_utils/test_get_operation_ast.py rename to graphql/utils/tests/test_get_operation_ast.py index 3dd04310..1147f67d 100644 --- a/tests/core_utils/test_get_operation_ast.py +++ b/graphql/utils/tests/test_get_operation_ast.py @@ -1,5 +1,5 @@ -from graphql.core import parse -from graphql.core.utils.get_operation_ast import get_operation_ast +from graphql import parse +from graphql.utils.get_operation_ast import get_operation_ast def test_gets_an_operation_from_a_simple_document(): diff --git a/tests/core_utils/test_schema_printer.py b/graphql/utils/tests/test_schema_printer.py similarity index 86% rename from tests/core_utils/test_schema_printer.py rename to graphql/utils/tests/test_schema_printer.py index 29a40558..8a2a5289 100644 --- a/tests/core_utils/test_schema_printer.py +++ b/graphql/utils/tests/test_schema_printer.py @@ -1,16 +1,14 @@ from collections import OrderedDict -from graphql.core.type import (GraphQLBoolean, GraphQLEnumType, - GraphQLInputObjectType, GraphQLInt, - GraphQLInterfaceType, GraphQLList, - GraphQLNonNull, GraphQLObjectType, - GraphQLScalarType, GraphQLSchema, GraphQLString, - GraphQLUnionType) -from graphql.core.type.definition import (GraphQLArgument, GraphQLEnumValue, - GraphQLField, - GraphQLInputObjectField) -from graphql.core.utils.schema_printer import (print_introspection_schema, - print_schema) +from graphql.type import (GraphQLBoolean, GraphQLEnumType, + GraphQLInputObjectType, GraphQLInt, + GraphQLInterfaceType, GraphQLList, GraphQLNonNull, + GraphQLObjectType, GraphQLScalarType, GraphQLSchema, + GraphQLString, GraphQLUnionType) +from graphql.type.definition import (GraphQLArgument, GraphQLEnumValue, + GraphQLField, GraphQLInputObjectField) +from graphql.utils.schema_printer import (print_introspection_schema, + print_schema) def print_for_test(schema): @@ -30,6 +28,10 @@ def print_single_field_schema(field_config): def test_prints_string_field(): output = print_single_field_schema(GraphQLField(GraphQLString)) assert output == ''' +schema { + query: Root +} + type Root { singleField: String } @@ -39,6 +41,10 @@ def test_prints_string_field(): def test_prints_list_string_field(): output = print_single_field_schema(GraphQLField(GraphQLList(GraphQLString))) assert output == ''' +schema { + query: Root +} + type Root { singleField: [String] } @@ -48,6 +54,10 @@ def test_prints_list_string_field(): def test_prints_non_null_list_string_field(): output = print_single_field_schema(GraphQLField(GraphQLNonNull(GraphQLList(GraphQLString)))) assert output == ''' +schema { + query: Root +} + type Root { singleField: [String]! } @@ -57,6 +67,10 @@ def test_prints_non_null_list_string_field(): def test_prints_list_non_null_string_field(): output = print_single_field_schema(GraphQLField((GraphQLList(GraphQLNonNull(GraphQLString))))) assert output == ''' +schema { + query: Root +} + type Root { singleField: [String!] } @@ -66,6 +80,10 @@ def test_prints_list_non_null_string_field(): def test_prints_non_null_list_non_null_string_field(): output = print_single_field_schema(GraphQLField(GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLString))))) assert output == ''' +schema { + query: Root +} + type Root { singleField: [String!]! } @@ -92,6 +110,10 @@ def test_prints_object_field(): output = print_for_test(Schema) assert output == ''' +schema { + query: Root +} + type Foo { str: String } @@ -108,6 +130,10 @@ def test_prints_string_field_with_int_arg(): args={'argOne': GraphQLArgument(GraphQLInt)} )) assert output == ''' +schema { + query: Root +} + type Root { singleField(argOne: Int): String } @@ -120,6 +146,10 @@ def test_prints_string_field_with_int_arg_with_default(): args={'argOne': GraphQLArgument(GraphQLInt, default_value=2)} )) assert output == ''' +schema { + query: Root +} + type Root { singleField(argOne: Int = 2): String } @@ -132,6 +162,10 @@ def test_prints_string_field_with_non_null_int_arg(): args={'argOne': GraphQLArgument(GraphQLNonNull(GraphQLInt))} )) assert output == ''' +schema { + query: Root +} + type Root { singleField(argOne: Int!): String } @@ -148,6 +182,10 @@ def test_prints_string_field_with_multiple_args(): )) assert output == ''' +schema { + query: Root +} + type Root { singleField(argOne: Int, argTwo: String): String } @@ -165,6 +203,10 @@ def test_prints_string_field_with_multiple_args_first_is_default(): )) assert output == ''' +schema { + query: Root +} + type Root { singleField(argOne: Int = 1, argTwo: String, argThree: Boolean): String } @@ -182,6 +224,10 @@ def test_prints_string_field_with_multiple_args_second_is_default(): )) assert output == ''' +schema { + query: Root +} + type Root { singleField(argOne: Int, argTwo: String = "foo", argThree: Boolean): String } @@ -199,6 +245,10 @@ def test_prints_string_field_with_multiple_args_last_is_default(): )) assert output == ''' +schema { + query: Root +} + type Root { singleField(argOne: Int, argTwo: String, argThree: Boolean = false): String } @@ -229,10 +279,14 @@ def test_prints_interface(): } ) - Schema = GraphQLSchema(Root) + Schema = GraphQLSchema(Root, types=[BarType]) output = print_for_test(Schema) assert output == ''' +schema { + query: Root +} + type Bar implements Foo { str: String } @@ -279,10 +333,14 @@ def test_prints_multiple_interfaces(): } ) - Schema = GraphQLSchema(Root) + Schema = GraphQLSchema(Root, types=[BarType]) output = print_for_test(Schema) assert output == ''' +schema { + query: Root +} + interface Baaz { int: Int } @@ -341,6 +399,10 @@ def test_prints_unions(): output = print_for_test(Schema) assert output == ''' +schema { + query: Root +} + type Bar { str: String } @@ -379,6 +441,10 @@ def test_prints_input_type(): output = print_for_test(Schema) assert output == ''' +schema { + query: Root +} + input InputType { int: Int } @@ -406,6 +472,10 @@ def test_prints_custom_scalar(): output = print_for_test(Schema) assert output == ''' +schema { + query: Root +} + scalar Odd type Root { @@ -435,6 +505,10 @@ def test_print_enum(): output = print_for_test(Schema) assert output == ''' +schema { + query: Root +} + enum RGB { RED GREEN @@ -459,15 +533,34 @@ def test_prints_introspection_schema(): output = '\n' + print_introspection_schema(Schema) assert output == ''' +schema { + query: Root +} + +directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + type __Directive { name: String! description: String + locations: [__DirectiveLocation!]! args: [__InputValue!]! onOperation: Boolean! onFragment: Boolean! onField: Boolean! } +enum __DirectiveLocation { + QUERY + MUTATION + SUBSCRIPTION + FIELD + FRAGMENT_DEFINITION + FRAGMENT_SPREAD + INLINE_FRAGMENT +} + type __EnumValue { name: String! description: String diff --git a/graphql/utils/tests/test_type_comparators.py b/graphql/utils/tests/test_type_comparators.py new file mode 100644 index 00000000..59a5d7b7 --- /dev/null +++ b/graphql/utils/tests/test_type_comparators.py @@ -0,0 +1,115 @@ +from collections import OrderedDict + +from graphql.type import (GraphQLField, GraphQLFloat, GraphQLInt, + GraphQLInterfaceType, GraphQLList, GraphQLNonNull, + GraphQLObjectType, GraphQLSchema, GraphQLString, + GraphQLUnionType) + +from ..type_comparators import is_equal_type, is_type_sub_type_of + + +def _test_schema(field_type): + return GraphQLSchema( + query=GraphQLObjectType( + name='Query', + fields=OrderedDict([ + ('field', GraphQLField(field_type)), + ]) + ) + ) + + +def test_is_equal_type_same_reference_are_equal(): + assert is_equal_type(GraphQLString, GraphQLString) + + +def test_is_equal_type_int_and_float_are_not_equal(): + assert not is_equal_type(GraphQLInt, GraphQLFloat) + + +def test_is_equal_type_lists_of_same_type_are_equal(): + assert is_equal_type( + GraphQLList(GraphQLInt), + GraphQLList(GraphQLInt) + ) + + +def test_is_equal_type_lists_is_not_equal_to_item(): + assert not is_equal_type(GraphQLList(GraphQLInt), GraphQLInt) + + +def test_is_equal_type_nonnull_of_same_type_are_equal(): + assert is_equal_type( + GraphQLNonNull(GraphQLInt), + GraphQLNonNull(GraphQLInt) + ) + + +def test_is_equal_type_nonnull_is_not_equal_to_nullable(): + assert not is_equal_type(GraphQLNonNull(GraphQLInt), GraphQLInt) + + +def test_is_equal_type_nonnull_is_not_equal_to_nullable(): + assert not is_equal_type(GraphQLNonNull(GraphQLInt), GraphQLInt) + + +def test_is_type_sub_type_of_same_reference_is_subtype(): + schema = _test_schema(GraphQLString) + assert is_type_sub_type_of(schema, GraphQLString, GraphQLString) + + +def test_is_type_sub_type_of_int_is_not_subtype_of_float(): + schema = _test_schema(GraphQLString) + assert not is_type_sub_type_of(schema, GraphQLInt, GraphQLFloat) + + +def test_is_type_sub_type_of_non_null_is_subtype_of_nullable(): + schema = _test_schema(GraphQLString) + assert is_type_sub_type_of(schema, GraphQLNonNull(GraphQLInt), GraphQLInt) + + +def test_is_type_sub_type_of_nullable_is_not_subtype_of_non_null(): + schema = _test_schema(GraphQLString) + assert not is_type_sub_type_of(schema, GraphQLInt, GraphQLNonNull(GraphQLInt)) + + +def test_is_type_sub_type_of_item_is_not_subtype_of_list(): + schema = _test_schema(GraphQLString) + assert not is_type_sub_type_of(schema, GraphQLInt, GraphQLList(GraphQLInt)) + + +def test_is_type_sub_type_of_list_is_not_subtype_of_item(): + schema = _test_schema(GraphQLString) + assert not is_type_sub_type_of(schema, GraphQLList(GraphQLInt), GraphQLInt) + + +def test_is_type_sub_type_of_member_is_subtype_of_union(): + member = GraphQLObjectType( + name='Object', + is_type_of=lambda *_: True, + fields={ + 'field': GraphQLField(GraphQLString) + } + ) + union = GraphQLUnionType(name='Union', types=[member]) + schema = _test_schema(union) + assert is_type_sub_type_of(schema, member, union) + + +def test_is_type_sub_type_of_implementation_is_subtype_of_interface(): + iface = GraphQLInterfaceType( + name='Interface', + fields={ + 'field': GraphQLField(GraphQLString) + } + ) + impl = GraphQLObjectType( + name='Object', + is_type_of=lambda *_: True, + interfaces=[iface], + fields={ + 'field': GraphQLField(GraphQLString) + } + ) + schema = _test_schema(impl) + assert is_type_sub_type_of(schema, impl, iface) diff --git a/graphql/utils/type_comparators.py b/graphql/utils/type_comparators.py new file mode 100644 index 00000000..93ebb045 --- /dev/null +++ b/graphql/utils/type_comparators.py @@ -0,0 +1,69 @@ +from ..type.definition import (GraphQLInterfaceType, GraphQLList, + GraphQLNonNull, GraphQLObjectType, + GraphQLUnionType, is_abstract_type) + + +def is_equal_type(type_a, type_b): + if type_a is type_b: + return True + + if isinstance(type_a, GraphQLNonNull) and isinstance(type_b, GraphQLNonNull): + return is_equal_type(type_a.of_type, type_b.of_type) + + if isinstance(type_a, GraphQLList) and isinstance(type_b, GraphQLList): + return is_equal_type(type_a.of_type, type_b.of_type) + + return False + + +def is_type_sub_type_of(schema, maybe_subtype, super_type): + if maybe_subtype is super_type: + return True + + if isinstance(super_type, GraphQLNonNull): + if isinstance(maybe_subtype, GraphQLNonNull): + return is_type_sub_type_of(schema, maybe_subtype.of_type, super_type.of_type) + return False + elif isinstance(maybe_subtype, GraphQLNonNull): + return is_type_sub_type_of(schema, maybe_subtype.of_type, super_type) + + if isinstance(super_type, GraphQLList): + if isinstance(maybe_subtype, GraphQLList): + return is_type_sub_type_of(schema, maybe_subtype.of_type, super_type.of_type) + return False + elif isinstance(maybe_subtype, GraphQLList): + return False + + if is_abstract_type(super_type) and isinstance( + maybe_subtype, GraphQLObjectType) and schema.is_possible_type( + super_type, maybe_subtype): + return True + + return False + + +def do_types_overlap(schema, t1, t2): + # print 'do_types_overlap', t1, t2 + if t1 == t2: + # print '1' + return True + + if isinstance(t1, (GraphQLInterfaceType, GraphQLUnionType)): + if isinstance(t2, (GraphQLInterfaceType, GraphQLUnionType)): + # If both types are abstract, then determine if there is any intersection + # between possible concrete types of each. + s = any([schema.is_possible_type(t2, type) for type in schema.get_possible_types(t1)]) + # print '2',s + return s + # Determine if the latter type is a possible concrete type of the former. + r = schema.is_possible_type(t1, t2) + # print '3', r + return r + + if isinstance(t2, (GraphQLInterfaceType, GraphQLUnionType)): + t = schema.is_possible_type(t2, t1) + # print '4', t + return t + + # print '5' + return False diff --git a/graphql/core/utils/type_from_ast.py b/graphql/utils/type_from_ast.py similarity index 100% rename from graphql/core/utils/type_from_ast.py rename to graphql/utils/type_from_ast.py diff --git a/graphql/core/utils/type_info.py b/graphql/utils/type_info.py similarity index 100% rename from graphql/core/utils/type_info.py rename to graphql/utils/type_info.py diff --git a/graphql/core/utils/value_from_ast.py b/graphql/utils/value_from_ast.py similarity index 100% rename from graphql/core/utils/value_from_ast.py rename to graphql/utils/value_from_ast.py diff --git a/graphql/validation/__init__.py b/graphql/validation/__init__.py new file mode 100644 index 00000000..893af3e7 --- /dev/null +++ b/graphql/validation/__init__.py @@ -0,0 +1,4 @@ +from .validation import validate +from .rules import specified_rules + +__all__ = ['validate', 'specified_rules'] diff --git a/graphql/core/validation/rules/__init__.py b/graphql/validation/rules/__init__.py similarity index 100% rename from graphql/core/validation/rules/__init__.py rename to graphql/validation/rules/__init__.py diff --git a/graphql/core/validation/rules/arguments_of_correct_type.py b/graphql/validation/rules/arguments_of_correct_type.py similarity index 100% rename from graphql/core/validation/rules/arguments_of_correct_type.py rename to graphql/validation/rules/arguments_of_correct_type.py diff --git a/graphql/core/validation/rules/base.py b/graphql/validation/rules/base.py similarity index 100% rename from graphql/core/validation/rules/base.py rename to graphql/validation/rules/base.py diff --git a/graphql/core/validation/rules/default_values_of_correct_type.py b/graphql/validation/rules/default_values_of_correct_type.py similarity index 100% rename from graphql/core/validation/rules/default_values_of_correct_type.py rename to graphql/validation/rules/default_values_of_correct_type.py diff --git a/graphql/core/validation/rules/fields_on_correct_type.py b/graphql/validation/rules/fields_on_correct_type.py similarity index 82% rename from graphql/core/validation/rules/fields_on_correct_type.py rename to graphql/validation/rules/fields_on_correct_type.py index 4a5c06be..96367f7e 100644 --- a/graphql/core/validation/rules/fields_on_correct_type.py +++ b/graphql/validation/rules/fields_on_correct_type.py @@ -1,4 +1,9 @@ from collections import Counter, OrderedDict + +from ...error import GraphQLError +from ...type.definition import is_abstract_type +from .base import ValidationRule + try: # Python 2 from itertools import izip @@ -6,10 +11,6 @@ # Python 3 izip = zip -from ...error import GraphQLError -from ...type.definition import GraphQLObjectType, is_abstract_type -from .base import ValidationRule - class OrderedCounter(Counter, OrderedDict): pass @@ -24,10 +25,12 @@ def enter_Field(self, node, key, parent, path, ancestors): field_def = self.context.get_field_def() if not field_def: + # This isn't valid. Let's find suggestions, if any. suggested_types = [] if is_abstract_type(type): - suggested_types = get_sibling_interfaces_including_field(type, node.name.value) - suggested_types += get_implementations_including_field(type, node.name.value) + schema = self.context.get_schema() + suggested_types = get_sibling_interfaces_including_field(schema, type, node.name.value) + suggested_types += get_implementations_including_field(schema, type, node.name.value) self.context.report_error(GraphQLError( self.undefined_field_message(node.name.value, type.name, suggested_types), [node] @@ -41,24 +44,24 @@ def undefined_field_message(field_name, type, suggested_types): suggestions = ', '.join(['"{}"'.format(t) for t in suggested_types[:MAX_LENGTH]]) l_suggested_types = len(suggested_types) if l_suggested_types > MAX_LENGTH: - suggestions += ", and {} other types".format(l_suggested_types-MAX_LENGTH) + suggestions += ", and {} other types".format(l_suggested_types - MAX_LENGTH) message += " However, this field exists on {}.".format(suggestions) message += " Perhaps you meant to use an inline fragment?" return message -def get_implementations_including_field(type, field_name): +def get_implementations_including_field(schema, type, field_name): '''Return implementations of `type` that include `fieldName` as a valid field.''' - return sorted(map(lambda t: t.name, filter(lambda t: field_name in t.get_fields(), type.get_possible_types()))) + return sorted(map(lambda t: t.name, filter(lambda t: field_name in t.get_fields(), schema.get_possible_types(type)))) -def get_sibling_interfaces_including_field(type, field_name): +def get_sibling_interfaces_including_field(schema, type, field_name): '''Go through all of the implementations of type, and find other interaces that they implement. If those interfaces include `field` as a valid field, return them, sorted by how often the implementations include the other interface.''' - implementing_objects = filter(lambda t: isinstance(t, GraphQLObjectType), type.get_possible_types()) + implementing_objects = schema.get_possible_types(type) suggested_interfaces = OrderedCounter() for t in implementing_objects: for i in t.get_interfaces(): diff --git a/graphql/core/validation/rules/fragments_on_composite_types.py b/graphql/validation/rules/fragments_on_composite_types.py similarity index 100% rename from graphql/core/validation/rules/fragments_on_composite_types.py rename to graphql/validation/rules/fragments_on_composite_types.py diff --git a/graphql/core/validation/rules/known_argument_names.py b/graphql/validation/rules/known_argument_names.py similarity index 100% rename from graphql/core/validation/rules/known_argument_names.py rename to graphql/validation/rules/known_argument_names.py diff --git a/graphql/core/validation/rules/known_directives.py b/graphql/validation/rules/known_directives.py similarity index 51% rename from graphql/core/validation/rules/known_directives.py rename to graphql/validation/rules/known_directives.py index 48c87cea..8e4a21a6 100644 --- a/graphql/core/validation/rules/known_directives.py +++ b/graphql/validation/rules/known_directives.py @@ -1,5 +1,6 @@ from ...error import GraphQLError from ...language import ast +from ...type.directives import DirectiveLocation from .base import ValidationRule @@ -18,23 +19,15 @@ def enter_Directive(self, node, key, parent, path, ancestors): )) applied_to = ancestors[-1] - - if isinstance(applied_to, ast.OperationDefinition) and not directive_def.on_operation: + candidate_location = get_location_for_applied_node(applied_to) + if not candidate_location: self.context.report_error(GraphQLError( - self.misplaced_directive_message(node.name.value, 'operation'), + self.misplaced_directive_message(node.name.value, node.type), [node] )) - - elif isinstance(applied_to, ast.Field) and not directive_def.on_field: + elif candidate_location not in directive_def.locations: self.context.report_error(GraphQLError( - self.misplaced_directive_message(node.name.value, 'field'), - [node] - )) - - elif (isinstance(applied_to, (ast.FragmentSpread, ast.InlineFragment, ast.FragmentDefinition)) and - not directive_def.on_fragment): - self.context.report_error(GraphQLError( - self.misplaced_directive_message(node.name.value, 'fragment'), + self.misplaced_directive_message(node.name.value, candidate_location), [node] )) @@ -43,5 +36,29 @@ def unknown_directive_message(directive_name): return 'Unknown directive "{}".'.format(directive_name) @staticmethod - def misplaced_directive_message(directive_name, placement): - return 'Directive "{}" may not be used on "{}".'.format(directive_name, placement) + def misplaced_directive_message(directive_name, location): + return 'Directive "{}" may not be used on "{}".'.format(directive_name, location) + + +_operation_definition_map = { + 'query': DirectiveLocation.QUERY, + 'mutation': DirectiveLocation.MUTATION, + 'subscription': DirectiveLocation.SUBSCRIPTION, +} + + +def get_location_for_applied_node(applied_to): + if isinstance(applied_to, ast.OperationDefinition): + return _operation_definition_map.get(applied_to.operation) + + elif isinstance(applied_to, ast.Field): + return DirectiveLocation.FIELD + + elif isinstance(applied_to, ast.FragmentSpread): + return DirectiveLocation.FRAGMENT_SPREAD + + elif isinstance(applied_to, ast.InlineFragment): + return DirectiveLocation.INLINE_FRAGMENT + + elif isinstance(applied_to, ast.FragmentDefinition): + return DirectiveLocation.FRAGMENT_DEFINITION diff --git a/graphql/core/validation/rules/known_fragment_names.py b/graphql/validation/rules/known_fragment_names.py similarity index 100% rename from graphql/core/validation/rules/known_fragment_names.py rename to graphql/validation/rules/known_fragment_names.py diff --git a/graphql/core/validation/rules/known_type_names.py b/graphql/validation/rules/known_type_names.py similarity index 100% rename from graphql/core/validation/rules/known_type_names.py rename to graphql/validation/rules/known_type_names.py diff --git a/graphql/core/validation/rules/lone_anonymous_operation.py b/graphql/validation/rules/lone_anonymous_operation.py similarity index 100% rename from graphql/core/validation/rules/lone_anonymous_operation.py rename to graphql/validation/rules/lone_anonymous_operation.py diff --git a/graphql/core/validation/rules/no_fragment_cycles.py b/graphql/validation/rules/no_fragment_cycles.py similarity index 100% rename from graphql/core/validation/rules/no_fragment_cycles.py rename to graphql/validation/rules/no_fragment_cycles.py diff --git a/graphql/core/validation/rules/no_undefined_variables.py b/graphql/validation/rules/no_undefined_variables.py similarity index 100% rename from graphql/core/validation/rules/no_undefined_variables.py rename to graphql/validation/rules/no_undefined_variables.py diff --git a/graphql/core/validation/rules/no_unused_fragments.py b/graphql/validation/rules/no_unused_fragments.py similarity index 100% rename from graphql/core/validation/rules/no_unused_fragments.py rename to graphql/validation/rules/no_unused_fragments.py diff --git a/graphql/core/validation/rules/no_unused_variables.py b/graphql/validation/rules/no_unused_variables.py similarity index 100% rename from graphql/core/validation/rules/no_unused_variables.py rename to graphql/validation/rules/no_unused_variables.py diff --git a/graphql/core/validation/rules/overlapping_fields_can_be_merged.py b/graphql/validation/rules/overlapping_fields_can_be_merged.py similarity index 60% rename from graphql/core/validation/rules/overlapping_fields_can_be_merged.py rename to graphql/validation/rules/overlapping_fields_can_be_merged.py index 6bae06a6..587d815c 100644 --- a/graphql/core/validation/rules/overlapping_fields_can_be_merged.py +++ b/graphql/validation/rules/overlapping_fields_can_be_merged.py @@ -5,8 +5,9 @@ from ...language.printer import print_ast from ...pyutils.default_ordered_dict import DefaultOrderedDict from ...pyutils.pair_set import PairSet -from ...type.definition import (GraphQLInterfaceType, GraphQLObjectType, - get_named_type) +from ...type.definition import (GraphQLInterfaceType, GraphQLList, + GraphQLNonNull, GraphQLObjectType, + get_named_type, is_leaf_type) from ...utils.type_comparators import is_equal_type from ...utils.type_from_ast import type_from_ast from .base import ValidationRule @@ -19,7 +20,7 @@ def __init__(self, context): super(OverlappingFieldsCanBeMerged, self).__init__(context) self.compared_set = PairSet() - def find_conflicts(self, field_map): + def find_conflicts(self, parent_fields_are_mutually_exclusive, field_map): conflicts = [] for response_name, fields in field_map.items(): field_len = len(fields) @@ -28,13 +29,18 @@ def find_conflicts(self, field_map): for field_a in fields: for field_b in fields: - conflict = self.find_conflict(response_name, field_a, field_b) + conflict = self.find_conflict( + parent_fields_are_mutually_exclusive, + response_name, + field_a, + field_b + ) if conflict: conflicts.append(conflict) return conflicts - def find_conflict(self, response_name, field1, field2): + def find_conflict(self, parent_fields_are_mutually_exclusive, response_name, field1, field2): parent_type1, ast1, def1 = field1 parent_type2, ast2, def2 = field2 @@ -42,50 +48,78 @@ def find_conflict(self, response_name, field1, field2): if ast1 is ast2: return - # If the statically known parent types could not possibly apply at the same - # time, then it is safe to permit them to diverge as they will not present - # any ambiguity by differing. - # It is known that two parent types could never overlap if they are - # different Object types. Interface or Union types might overlap - if not - # in the current state of the schema, then perhaps in some future version, - # thus may not safely diverge. - if parent_type1 != parent_type2 and \ - isinstance(parent_type1, GraphQLObjectType) and \ - isinstance(parent_type2, GraphQLObjectType): - return + # Memoize, do not report the same issue twice. + # Note: Two overlapping ASTs could be encountered both when + # `parentFieldsAreMutuallyExclusive` is true and is false, which could + # produce different results (when `true` being a subset of `false`). + # However we do not need to include this piece of information when + # memoizing since this rule visits leaf fields before their parent fields, + # ensuring that `parentFieldsAreMutuallyExclusive` is `false` the first + # time two overlapping fields are encountered, ensuring that the full + # set of validation rules are always checked when necessary. + + # if parent_type1 != parent_type2 and \ + # isinstance(parent_type1, GraphQLObjectType) and \ + # isinstance(parent_type2, GraphQLObjectType): + # return if self.compared_set.has(ast1, ast2): return self.compared_set.add(ast1, ast2) - name1 = ast1.name.value - name2 = ast2.name.value - - if name1 != name2: - return ( - (response_name, '{} and {} are different fields'.format(name1, name2)), - [ast1], - [ast2] - ) - + # The return type for each field. type1 = def1 and def1.type type2 = def2 and def2.type - if type1 and type2 and not self.same_type(type1, type2): - return ( - (response_name, 'they return differing types {} and {}'.format(type1, type2)), - [ast1], - [ast2] + # If it is known that two fields could not possibly apply at the same + # time, due to the parent types, then it is safe to permit them to diverge + # in aliased field or arguments used as they will not present any ambiguity + # by differing. + # It is known that two parent types could never overlap if they are + # different Object types. Interface or Union types might overlap - if not + # in the current state of the schema, then perhaps in some future version, + # thus may not safely diverge. + + fields_are_mutually_exclusive = ( + parent_fields_are_mutually_exclusive or ( + parent_type1 != parent_type2 and + isinstance(parent_type1, GraphQLObjectType) and + isinstance(parent_type2, GraphQLObjectType) ) + ) + + if not fields_are_mutually_exclusive: + name1 = ast1.name.value + name2 = ast2.name.value + + if name1 != name2: + return ( + (response_name, '{} and {} are different fields'.format(name1, name2)), + [ast1], + [ast2] + ) + + if not self.same_arguments(ast1.arguments, ast2.arguments): + return ( + (response_name, 'they have differing arguments'), + [ast1], + [ast2] + ) - if not self.same_arguments(ast1.arguments, ast2.arguments): + if type1 and type2 and do_types_conflict(type1, type2): return ( - (response_name, 'they have differing arguments'), + (response_name, 'they return conflicting types {} and {}'.format(type1, type2)), [ast1], [ast2] ) + subfield_map = self.get_subfield_map(ast1, type1, ast2, type2) + if subfield_map: + conflicts = self.find_conflicts(fields_are_mutually_exclusive, subfield_map) + return self.subfield_conflicts(conflicts, response_name, ast1, ast2) + + def get_subfield_map(self, ast1, type1, ast2, type2): selection_set1 = ast1.selection_set selection_set2 = ast2.selection_set @@ -104,22 +138,26 @@ def find_conflict(self, response_name, field1, field2): visited_fragment_names, subfield_map ) + return subfield_map - conflicts = self.find_conflicts(subfield_map) - if conflicts: - return ( - (response_name, [conflict[0] for conflict in conflicts]), - tuple(itertools.chain([ast1], *[conflict[1] for conflict in conflicts])), - tuple(itertools.chain([ast2], *[conflict[2] for conflict in conflicts])) - ) + def subfield_conflicts(self, conflicts, response_name, ast1, ast2): + if conflicts: + return ( + (response_name, [conflict[0] for conflict in conflicts]), + tuple(itertools.chain([ast1], *[conflict[1] for conflict in conflicts])), + tuple(itertools.chain([ast2], *[conflict[2] for conflict in conflicts])) + ) def leave_SelectionSet(self, node, key, parent, path, ancestors): + # Note: we validate on the reverse traversal so deeper conflicts will be + # caught first, for correct calculation of mutual exclusivity and for + # clearer error messages. field_map = self.collect_field_asts_and_defs( self.context.get_parent_type(), node ) - conflicts = self.find_conflicts(field_map) + conflicts = self.find_conflicts(False, field_map) if conflicts: for (reason_name, reason), fields1, fields2 in conflicts: self.context.report_error( @@ -228,3 +266,30 @@ def reason_message(cls, reason): for reason_name, sub_reason in reason) return reason + + +def do_types_conflict(type1, type2): + if isinstance(type1, GraphQLList): + if isinstance(type2, GraphQLList): + return do_types_conflict(type1.of_type, type2.of_type) + return True + + if isinstance(type2, GraphQLList): + if isinstance(type1, GraphQLList): + return do_types_conflict(type1.of_type, type2.of_type) + return True + + if isinstance(type1, GraphQLNonNull): + if isinstance(type2, GraphQLNonNull): + return do_types_conflict(type1.of_type, type2.of_type) + return True + + if isinstance(type2, GraphQLNonNull): + if isinstance(type1, GraphQLNonNull): + return do_types_conflict(type1.of_type, type2.of_type) + return True + + if is_leaf_type(type1) or is_leaf_type(type2): + return type1 != type2 + + return False diff --git a/graphql/core/validation/rules/possible_fragment_spreads.py b/graphql/validation/rules/possible_fragment_spreads.py similarity index 87% rename from graphql/core/validation/rules/possible_fragment_spreads.py rename to graphql/validation/rules/possible_fragment_spreads.py index d7709995..b9cc4165 100644 --- a/graphql/core/validation/rules/possible_fragment_spreads.py +++ b/graphql/validation/rules/possible_fragment_spreads.py @@ -9,7 +9,8 @@ class PossibleFragmentSpreads(ValidationRule): def enter_InlineFragment(self, node, key, parent, path, ancestors): frag_type = self.context.get_type() parent_type = self.context.get_parent_type() - if frag_type and parent_type and not do_types_overlap(frag_type, parent_type): + schema = self.context.get_schema() + if frag_type and parent_type and not do_types_overlap(schema, frag_type, parent_type): self.context.report_error(GraphQLError( self.type_incompatible_anon_spread_message(parent_type, frag_type), [node] @@ -19,7 +20,8 @@ def enter_FragmentSpread(self, node, key, parent, path, ancestors): frag_name = node.name.value frag_type = self.get_fragment_type(self.context, frag_name) parent_type = self.context.get_parent_type() - if frag_type and parent_type and not do_types_overlap(frag_type, parent_type): + schema = self.context.get_schema() + if frag_type and parent_type and not do_types_overlap(schema, frag_type, parent_type): self.context.report_error(GraphQLError( self.type_incompatible_spread_message(frag_name, parent_type, frag_type), [node] diff --git a/graphql/core/validation/rules/provided_non_null_arguments.py b/graphql/validation/rules/provided_non_null_arguments.py similarity index 100% rename from graphql/core/validation/rules/provided_non_null_arguments.py rename to graphql/validation/rules/provided_non_null_arguments.py diff --git a/graphql/core/validation/rules/scalar_leafs.py b/graphql/validation/rules/scalar_leafs.py similarity index 100% rename from graphql/core/validation/rules/scalar_leafs.py rename to graphql/validation/rules/scalar_leafs.py diff --git a/graphql/core/validation/rules/unique_argument_names.py b/graphql/validation/rules/unique_argument_names.py similarity index 100% rename from graphql/core/validation/rules/unique_argument_names.py rename to graphql/validation/rules/unique_argument_names.py diff --git a/graphql/core/validation/rules/unique_fragment_names.py b/graphql/validation/rules/unique_fragment_names.py similarity index 100% rename from graphql/core/validation/rules/unique_fragment_names.py rename to graphql/validation/rules/unique_fragment_names.py diff --git a/graphql/core/validation/rules/unique_input_field_names.py b/graphql/validation/rules/unique_input_field_names.py similarity index 100% rename from graphql/core/validation/rules/unique_input_field_names.py rename to graphql/validation/rules/unique_input_field_names.py diff --git a/graphql/core/validation/rules/unique_operation_names.py b/graphql/validation/rules/unique_operation_names.py similarity index 100% rename from graphql/core/validation/rules/unique_operation_names.py rename to graphql/validation/rules/unique_operation_names.py diff --git a/graphql/core/validation/rules/unique_variable_names.py b/graphql/validation/rules/unique_variable_names.py similarity index 100% rename from graphql/core/validation/rules/unique_variable_names.py rename to graphql/validation/rules/unique_variable_names.py diff --git a/graphql/core/validation/rules/variables_are_input_types.py b/graphql/validation/rules/variables_are_input_types.py similarity index 100% rename from graphql/core/validation/rules/variables_are_input_types.py rename to graphql/validation/rules/variables_are_input_types.py diff --git a/graphql/core/validation/rules/variables_in_allowed_position.py b/graphql/validation/rules/variables_in_allowed_position.py similarity index 73% rename from graphql/core/validation/rules/variables_in_allowed_position.py rename to graphql/validation/rules/variables_in_allowed_position.py index 67d9c8ba..4117e0f8 100644 --- a/graphql/core/validation/rules/variables_in_allowed_position.py +++ b/graphql/validation/rules/variables_in_allowed_position.py @@ -24,8 +24,14 @@ def leave_OperationDefinition(self, operation, key, parent, path, ancestors): var_name = node.name.value var_def = self.var_def_map.get(var_name) if var_def and type: - var_type = type_from_ast(self.context.get_schema(), var_def.type) - if var_type and not is_type_sub_type_of(self.effective_type(var_type, var_def), type): + # A var type is allowed if it is the same or more strict (e.g. is + # a subtype of) than the expected type. It can be more strict if + # the variable type is non-null when the expected type is nullable. + # If both are list types, the variable item type can be more strict + # than the expected item type (contravariant). + schema = self.context.get_schema() + var_type = type_from_ast(schema, var_def.type) + if var_type and not is_type_sub_type_of(schema, self.effective_type(var_type, var_def), type): self.context.report_error(GraphQLError( self.bad_var_pos_message(var_name, var_type, type), [var_def, node] diff --git a/tests/core_validation/__init__.py b/graphql/validation/tests/__init__.py similarity index 100% rename from tests/core_validation/__init__.py rename to graphql/validation/tests/__init__.py diff --git a/tests/core_validation/test_arguments_of_correct_type.py b/graphql/validation/tests/test_arguments_of_correct_type.py similarity index 99% rename from tests/core_validation/test_arguments_of_correct_type.py rename to graphql/validation/tests/test_arguments_of_correct_type.py index 1f2a71d1..828aa61c 100644 --- a/tests/core_validation/test_arguments_of_correct_type.py +++ b/graphql/validation/tests/test_arguments_of_correct_type.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import ArgumentsOfCorrectType +from graphql.language.location import SourceLocation +from graphql.validation.rules import ArgumentsOfCorrectType + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_default_values_of_correct_type.py b/graphql/validation/tests/test_default_values_of_correct_type.py similarity index 95% rename from tests/core_validation/test_default_values_of_correct_type.py rename to graphql/validation/tests/test_default_values_of_correct_type.py index d9065a6b..25d2d55d 100644 --- a/tests/core_validation/test_default_values_of_correct_type.py +++ b/graphql/validation/tests/test_default_values_of_correct_type.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import DefaultValuesOfCorrectType +from graphql.language.location import SourceLocation +from graphql.validation.rules import DefaultValuesOfCorrectType + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_fields_on_correct_type.py b/graphql/validation/tests/test_fields_on_correct_type.py similarity index 89% rename from tests/core_validation/test_fields_on_correct_type.py rename to graphql/validation/tests/test_fields_on_correct_type.py index 88d3574a..7c9376df 100644 --- a/tests/core_validation/test_fields_on_correct_type.py +++ b/graphql/validation/tests/test_fields_on_correct_type.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import FieldsOnCorrectType +from graphql.language.location import SourceLocation +from graphql.validation.rules import FieldsOnCorrectType + from .utils import expect_fails_rule, expect_passes_rule @@ -187,11 +188,11 @@ def test_defined_on_implementors_queried_on_union(): } ''', [ undefined_field( - 'name', - 'CatOrDog', - ['Being', 'Pet', 'Canine', 'Cat', 'Dog'], - 3, - 9 + 'name', + 'CatOrDog', + ['Being', 'Pet', 'Canine', 'Cat', 'Dog'], + 3, + 9 ) ]) @@ -217,17 +218,17 @@ def test_fields_correct_type_no_suggestion(): def test_fields_correct_type_no_small_number_suggestions(): message = FieldsOnCorrectType.undefined_field_message('T', 'f', ['A', 'B']) assert message == ( - 'Cannot query field "T" on type "f". ' + - 'However, this field exists on "A", "B". ' + - 'Perhaps you meant to use an inline fragment?' + 'Cannot query field "T" on type "f". ' + + 'However, this field exists on "A", "B". ' + + 'Perhaps you meant to use an inline fragment?' ) def test_fields_correct_type_lot_suggestions(): message = FieldsOnCorrectType.undefined_field_message('T', 'f', ['A', 'B', 'C', 'D', 'E', 'F']) assert message == ( - 'Cannot query field "T" on type "f". ' + - 'However, this field exists on "A", "B", "C", "D", "E", ' + - 'and 1 other types. '+ - 'Perhaps you meant to use an inline fragment?' + 'Cannot query field "T" on type "f". ' + + 'However, this field exists on "A", "B", "C", "D", "E", ' + + 'and 1 other types. ' + + 'Perhaps you meant to use an inline fragment?' ) diff --git a/tests/core_validation/test_fragments_on_composite_types.py b/graphql/validation/tests/test_fragments_on_composite_types.py similarity index 95% rename from tests/core_validation/test_fragments_on_composite_types.py rename to graphql/validation/tests/test_fragments_on_composite_types.py index 4e96022c..531d74f4 100644 --- a/tests/core_validation/test_fragments_on_composite_types.py +++ b/graphql/validation/tests/test_fragments_on_composite_types.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import FragmentsOnCompositeTypes +from graphql.language.location import SourceLocation +from graphql.validation.rules import FragmentsOnCompositeTypes + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_known_argument_names.py b/graphql/validation/tests/test_known_argument_names.py similarity index 96% rename from tests/core_validation/test_known_argument_names.py rename to graphql/validation/tests/test_known_argument_names.py index 741ff607..ed8e2569 100644 --- a/tests/core_validation/test_known_argument_names.py +++ b/graphql/validation/tests/test_known_argument_names.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import KnownArgumentNames +from graphql.language.location import SourceLocation +from graphql.validation.rules import KnownArgumentNames + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_known_directives.py b/graphql/validation/tests/test_known_directives.py similarity index 87% rename from tests/core_validation/test_known_directives.py rename to graphql/validation/tests/test_known_directives.py index c3c1d5d8..8309f480 100644 --- a/tests/core_validation/test_known_directives.py +++ b/graphql/validation/tests/test_known_directives.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import KnownDirectives +from graphql.language.location import SourceLocation +from graphql.validation.rules import KnownDirectives + from .utils import expect_fails_rule, expect_passes_rule @@ -93,8 +94,8 @@ def test_with_misplaced_directives(): ...Frag @operationOnly } ''', [ - misplaced_directive('include', 'operation', 2, 17), - misplaced_directive('operationOnly', 'field', 3, 14), - misplaced_directive('operationOnly', 'fragment', 4, 17), + misplaced_directive('include', 'QUERY', 2, 17), + misplaced_directive('operationOnly', 'FIELD', 3, 14), + misplaced_directive('operationOnly', 'FRAGMENT_SPREAD', 4, 17), ]) diff --git a/tests/core_validation/test_known_fragment_names.py b/graphql/validation/tests/test_known_fragment_names.py similarity index 91% rename from tests/core_validation/test_known_fragment_names.py rename to graphql/validation/tests/test_known_fragment_names.py index b4a83e80..3364b2ee 100644 --- a/tests/core_validation/test_known_fragment_names.py +++ b/graphql/validation/tests/test_known_fragment_names.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import KnownFragmentNames +from graphql.language.location import SourceLocation +from graphql.validation.rules import KnownFragmentNames + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_known_type_names.py b/graphql/validation/tests/test_known_type_names.py similarity index 92% rename from tests/core_validation/test_known_type_names.py rename to graphql/validation/tests/test_known_type_names.py index 51152e82..17736532 100644 --- a/tests/core_validation/test_known_type_names.py +++ b/graphql/validation/tests/test_known_type_names.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import KnownTypeNames +from graphql.language.location import SourceLocation +from graphql.validation.rules import KnownTypeNames + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_lone_anonymous_operation.py b/graphql/validation/tests/test_lone_anonymous_operation.py similarity index 92% rename from tests/core_validation/test_lone_anonymous_operation.py rename to graphql/validation/tests/test_lone_anonymous_operation.py index d70e8e3a..67707580 100644 --- a/tests/core_validation/test_lone_anonymous_operation.py +++ b/graphql/validation/tests/test_lone_anonymous_operation.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import LoneAnonymousOperation +from graphql.language.location import SourceLocation +from graphql.validation.rules import LoneAnonymousOperation + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_no_fragment_cycles.py b/graphql/validation/tests/test_no_fragment_cycles.py similarity index 97% rename from tests/core_validation/test_no_fragment_cycles.py rename to graphql/validation/tests/test_no_fragment_cycles.py index 9d0e35fd..4403a35e 100644 --- a/tests/core_validation/test_no_fragment_cycles.py +++ b/graphql/validation/tests/test_no_fragment_cycles.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation as L -from graphql.core.validation.rules import NoFragmentCycles +from graphql.language.location import SourceLocation as L +from graphql.validation.rules import NoFragmentCycles + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_no_undefined_variables.py b/graphql/validation/tests/test_no_undefined_variables.py similarity index 98% rename from tests/core_validation/test_no_undefined_variables.py rename to graphql/validation/tests/test_no_undefined_variables.py index 473542a5..11e566b7 100644 --- a/tests/core_validation/test_no_undefined_variables.py +++ b/graphql/validation/tests/test_no_undefined_variables.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import NoUndefinedVariables +from graphql.language.location import SourceLocation +from graphql.validation.rules import NoUndefinedVariables + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_no_unused_fragments.py b/graphql/validation/tests/test_no_unused_fragments.py similarity index 96% rename from tests/core_validation/test_no_unused_fragments.py rename to graphql/validation/tests/test_no_unused_fragments.py index 363184b2..a27df046 100644 --- a/tests/core_validation/test_no_unused_fragments.py +++ b/graphql/validation/tests/test_no_unused_fragments.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import NoUnusedFragments +from graphql.language.location import SourceLocation +from graphql.validation.rules import NoUnusedFragments + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_no_unused_variables.py b/graphql/validation/tests/test_no_unused_variables.py similarity index 97% rename from tests/core_validation/test_no_unused_variables.py rename to graphql/validation/tests/test_no_unused_variables.py index ddcfaae1..3060b102 100644 --- a/tests/core_validation/test_no_unused_variables.py +++ b/graphql/validation/tests/test_no_unused_variables.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import NoUnusedVariables +from graphql.language.location import SourceLocation +from graphql.validation.rules import NoUnusedVariables + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_overlapping_fields_can_be_merged.py b/graphql/validation/tests/test_overlapping_fields_can_be_merged.py similarity index 61% rename from tests/core_validation/test_overlapping_fields_can_be_merged.py rename to graphql/validation/tests/test_overlapping_fields_can_be_merged.py index e9c539f9..97c3a02c 100644 --- a/tests/core_validation/test_overlapping_fields_can_be_merged.py +++ b/graphql/validation/tests/test_overlapping_fields_can_be_merged.py @@ -1,12 +1,13 @@ -from graphql.core.language.location import SourceLocation as L -from graphql.core.type.definition import (GraphQLArgument, GraphQLField, - GraphQLInterfaceType, GraphQLList, - GraphQLNonNull, GraphQLObjectType) -from graphql.core.type.scalars import GraphQLID, GraphQLInt, GraphQLString -from graphql.core.type.schema import GraphQLSchema -from graphql.core.validation.rules import OverlappingFieldsCanBeMerged +from graphql.language.location import SourceLocation as L +from graphql.type.definition import (GraphQLArgument, GraphQLField, + GraphQLInterfaceType, GraphQLList, + GraphQLNonNull, GraphQLObjectType) +from graphql.type.scalars import GraphQLID, GraphQLInt, GraphQLString +from graphql.type.schema import GraphQLSchema +from graphql.validation.rules import OverlappingFieldsCanBeMerged + from .utils import (expect_fails_rule, expect_fails_rule_with_schema, - expect_passes_rule, expect_passes_rule_with_schema) + expect_passes_rule, expect_passes_rule_with_schema) def fields_conflict(reason_name, reason, *locations): @@ -291,26 +292,48 @@ def test_reports_deep_conflict_to_nearest_common_ancestor(): ], sort_list=False) -SomeBox = GraphQLInterfaceType('SomeBox', { - 'unrelatedField': GraphQLField(GraphQLString) -}, resolve_type=lambda *_: StringBox) - -StringBox = GraphQLObjectType('StringBox', { - 'scalar': GraphQLField(GraphQLString), - 'unrelatedField': GraphQLField(GraphQLString) -}, interfaces=[SomeBox]) - -IntBox = GraphQLObjectType('IntBox', { - 'scalar': GraphQLField(GraphQLInt), - 'unrelatedField': GraphQLField(GraphQLString) -}, interfaces=[SomeBox]) +SomeBox = GraphQLInterfaceType( + 'SomeBox', + fields=lambda: { + 'deepBox': GraphQLField(SomeBox), + 'unrelatedField': GraphQLField(GraphQLString) + }, + resolve_type=lambda *_: StringBox +) + +StringBox = GraphQLObjectType( + 'StringBox', + fields=lambda: { + 'scalar': GraphQLField(GraphQLString), + 'deepBox': GraphQLField(StringBox), + 'unrelatedField': GraphQLField(GraphQLString), + 'listStringBox': GraphQLField(GraphQLList(StringBox)), + 'stringBox': GraphQLField(StringBox), + 'intBox': GraphQLField(IntBox), + }, + interfaces=[SomeBox] +) + +IntBox = GraphQLObjectType( + 'IntBox', + fields=lambda: { + 'scalar': GraphQLField(GraphQLInt), + 'deepBox': GraphQLField(IntBox), + 'unrelatedField': GraphQLField(GraphQLString), + 'listStringBox': GraphQLField(GraphQLList(StringBox)), + 'stringBox': GraphQLField(StringBox), + 'intBox': GraphQLField(IntBox), + }, + interfaces=[SomeBox] +) NonNullStringBox1 = GraphQLInterfaceType('NonNullStringBox1', { - 'scalar': GraphQLField(GraphQLNonNull(GraphQLString)) + 'scalar': GraphQLField(GraphQLNonNull(GraphQLString)), }, resolve_type=lambda *_: StringBox) NonNullStringBox1Impl = GraphQLObjectType('NonNullStringBox1Impl', { 'scalar': GraphQLField(GraphQLNonNull(GraphQLString)), + 'deepBox': GraphQLField(StringBox), 'unrelatedField': GraphQLField(GraphQLString) }, interfaces=[SomeBox, NonNullStringBox1]) @@ -320,7 +343,8 @@ def test_reports_deep_conflict_to_nearest_common_ancestor(): NonNullStringBox2Impl = GraphQLObjectType('NonNullStringBox2Impl', { 'scalar': GraphQLField(GraphQLNonNull(GraphQLString)), - 'unrelatedField': GraphQLField(GraphQLString) + 'unrelatedField': GraphQLField(GraphQLString), + 'deepBox': GraphQLField(StringBox), }, interfaces=[SomeBox, NonNullStringBox2]) Connection = GraphQLObjectType('Connection', { @@ -332,10 +356,13 @@ def test_reports_deep_conflict_to_nearest_common_ancestor(): }))) }) -schema = GraphQLSchema(GraphQLObjectType('QueryRoot', { - 'someBox': GraphQLField(SomeBox), - 'connection': GraphQLField(Connection) -})) +schema = GraphQLSchema( + GraphQLObjectType('QueryRoot', { + 'someBox': GraphQLField(SomeBox), + 'connection': GraphQLField(Connection), + }), + types=[IntBox, StringBox, NonNullStringBox1Impl, NonNullStringBox2Impl] +) def test_conflicting_return_types_which_potentially_overlap(): @@ -352,22 +379,190 @@ def test_conflicting_return_types_which_potentially_overlap(): } ''', [ - fields_conflict('scalar', 'they return differing types Int and String!', L(5, 17), L(8, 17)) + fields_conflict('scalar', 'they return conflicting types Int and String!', L(5, 17), L(8, 17)) ], sort_list=False) -def test_allows_differing_return_types_which_cannot_overlap(): +def test_compatible_return_shapes_on_different_return_types(): + # In this case `deepBox` returns `SomeBox` in the first usage, and + # `StringBox` in the second usage. These return types are not the same! + # however this is valid because the return *shapes* are compatible. expect_passes_rule_with_schema(schema, OverlappingFieldsCanBeMerged, ''' - { + { someBox { - ...on IntBox { + ... on SomeBox { + deepBox { + unrelatedField + } + } + ... on StringBox { + deepBox { + unrelatedField + } + } + } + } + ''') + + +def test_disallows_differing_return_types_despite_no_overlap(): + expect_fails_rule_with_schema(schema, OverlappingFieldsCanBeMerged, ''' + { + someBox { + ... on IntBox { + scalar + } + ... on StringBox { + scalar + } + } + } + ''', [ + fields_conflict( + 'scalar', 'they return conflicting types Int and String', + L(5, 15), L(8, 15), + ) + ], sort_list=False) + + +def test_disallows_differing_return_type_nullability_despite_no_overlap(): + expect_fails_rule_with_schema(schema, OverlappingFieldsCanBeMerged, ''' + { + someBox { + ... on NonNullStringBox1 { + scalar + } + ... on StringBox { + scalar + } + } + } + ''', [ + fields_conflict( + 'scalar', + 'they return conflicting types String! and String', + L(5, 15), L(8, 15), + ) + ], sort_list=False) + + +def test_disallows_differing_return_type_list_despite_no_overlap_1(): + expect_fails_rule_with_schema(schema, OverlappingFieldsCanBeMerged, ''' + { + someBox { + ... on IntBox { + box: listStringBox { scalar + } } - ...on StringBox { + ... on StringBox { + box: stringBox { scalar + } } + } + } + ''', [ + fields_conflict( + 'box', + 'they return conflicting types [StringBox] and StringBox', + L(5, 15), + L(10, 15), + ) + ], sort_list=False) + + +def test_disallows_differing_return_type_list_despite_no_overlap_2(): + expect_fails_rule_with_schema(schema, OverlappingFieldsCanBeMerged, ''' + { + someBox { + ... on IntBox { + box: stringBox { + scalar + } + } + ... on StringBox { + box: listStringBox { + scalar + } + } + } + } + ''', [ + fields_conflict( + 'box', + 'they return conflicting types StringBox and [StringBox]', + L(5, 15), L(10, 15), + ) + ], sort_list=False) + + +def test_disallows_differing_subfields(): + expect_fails_rule_with_schema(schema, OverlappingFieldsCanBeMerged, ''' + { + someBox { + ... on IntBox { + box: stringBox { + val: scalar + val: unrelatedField + } + } + ... on StringBox { + box: stringBox { + val: scalar + } + } + } + } + ''', [ + fields_conflict( + 'val', + 'scalar and unrelatedField are different fields', + L(6, 17), L(7, 17), + ) + ], sort_list=False) + + +def test_disallows_differing_deep_return_types_despite_no_overlap(): + expect_fails_rule_with_schema(schema, OverlappingFieldsCanBeMerged, ''' + { + someBox { + ... on IntBox { + box: stringBox { + scalar + } + } + ... on StringBox { + box: intBox { + scalar + } + } + } + } + ''', [ + fields_conflict( + 'box', + [['scalar', 'they return conflicting types String and Int']], + L(5, 15), + L(6, 17), + L(10, 15), + L(11, 17), + ) + ], sort_list=False) + + +def test_allows_non_conflicting_overlaping_types(): + expect_passes_rule_with_schema(schema, OverlappingFieldsCanBeMerged, ''' + { + someBox { + ... on IntBox { + scalar: unrelatedField + } + ... on StringBox { + scalar + } + } } - } ''') diff --git a/tests/core_validation/test_possible_fragment_spreads.py b/graphql/validation/tests/test_possible_fragment_spreads.py similarity index 98% rename from tests/core_validation/test_possible_fragment_spreads.py rename to graphql/validation/tests/test_possible_fragment_spreads.py index f17ffd80..d236372e 100644 --- a/tests/core_validation/test_possible_fragment_spreads.py +++ b/graphql/validation/tests/test_possible_fragment_spreads.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import PossibleFragmentSpreads +from graphql.language.location import SourceLocation +from graphql.validation.rules import PossibleFragmentSpreads + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_provided_non_null_arguments.py b/graphql/validation/tests/test_provided_non_null_arguments.py similarity index 97% rename from tests/core_validation/test_provided_non_null_arguments.py rename to graphql/validation/tests/test_provided_non_null_arguments.py index 6ced6239..35a8a502 100644 --- a/tests/core_validation/test_provided_non_null_arguments.py +++ b/graphql/validation/tests/test_provided_non_null_arguments.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import ProvidedNonNullArguments +from graphql.language.location import SourceLocation +from graphql.validation.rules import ProvidedNonNullArguments + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_scalar_leafs.py b/graphql/validation/tests/test_scalar_leafs.py similarity index 96% rename from tests/core_validation/test_scalar_leafs.py rename to graphql/validation/tests/test_scalar_leafs.py index 8f7ac205..af10abe8 100644 --- a/tests/core_validation/test_scalar_leafs.py +++ b/graphql/validation/tests/test_scalar_leafs.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import ScalarLeafs +from graphql.language.location import SourceLocation +from graphql.validation.rules import ScalarLeafs + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_unique_argument_names.py b/graphql/validation/tests/test_unique_argument_names.py similarity index 95% rename from tests/core_validation/test_unique_argument_names.py rename to graphql/validation/tests/test_unique_argument_names.py index 13d72cf2..fb38a99f 100644 --- a/tests/core_validation/test_unique_argument_names.py +++ b/graphql/validation/tests/test_unique_argument_names.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import UniqueArgumentNames +from graphql.language.location import SourceLocation +from graphql.validation.rules import UniqueArgumentNames + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_unique_fragment_names.py b/graphql/validation/tests/test_unique_fragment_names.py similarity index 93% rename from tests/core_validation/test_unique_fragment_names.py rename to graphql/validation/tests/test_unique_fragment_names.py index 0780c848..c7c9e63f 100644 --- a/tests/core_validation/test_unique_fragment_names.py +++ b/graphql/validation/tests/test_unique_fragment_names.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import UniqueFragmentNames +from graphql.language.location import SourceLocation +from graphql.validation.rules import UniqueFragmentNames + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_unique_input_field_names.py b/graphql/validation/tests/test_unique_input_field_names.py similarity index 92% rename from tests/core_validation/test_unique_input_field_names.py rename to graphql/validation/tests/test_unique_input_field_names.py index abfabb87..f7af59ee 100644 --- a/tests/core_validation/test_unique_input_field_names.py +++ b/graphql/validation/tests/test_unique_input_field_names.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation as L -from graphql.core.validation.rules import UniqueInputFieldNames +from graphql.language.location import SourceLocation as L +from graphql.validation.rules import UniqueInputFieldNames + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_unique_operation_names.py b/graphql/validation/tests/test_unique_operation_names.py similarity index 94% rename from tests/core_validation/test_unique_operation_names.py rename to graphql/validation/tests/test_unique_operation_names.py index db262213..e222c7c9 100644 --- a/tests/core_validation/test_unique_operation_names.py +++ b/graphql/validation/tests/test_unique_operation_names.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import UniqueOperationNames +from graphql.language.location import SourceLocation +from graphql.validation.rules import UniqueOperationNames + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_unique_variable_names.py b/graphql/validation/tests/test_unique_variable_names.py similarity index 88% rename from tests/core_validation/test_unique_variable_names.py rename to graphql/validation/tests/test_unique_variable_names.py index cfefa6a8..b0276079 100644 --- a/tests/core_validation/test_unique_variable_names.py +++ b/graphql/validation/tests/test_unique_variable_names.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import UniqueVariableNames +from graphql.language.location import SourceLocation +from graphql.validation.rules import UniqueVariableNames + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_validation.py b/graphql/validation/tests/test_validation.py similarity index 84% rename from tests/core_validation/test_validation.py rename to graphql/validation/tests/test_validation.py index 25018a05..ead25d2d 100644 --- a/tests/core_validation/test_validation.py +++ b/graphql/validation/tests/test_validation.py @@ -1,7 +1,8 @@ -from graphql.core import parse, validate -from graphql.core.utils.type_info import TypeInfo -from graphql.core.validation import visit_using_rules -from graphql.core.validation.rules import specified_rules +from graphql import parse, validate +from graphql.utils.type_info import TypeInfo +from graphql.validation.rules import specified_rules +from graphql.validation.validation import visit_using_rules + from .utils import test_schema diff --git a/tests/core_validation/test_variables_are_input_types.py b/graphql/validation/tests/test_variables_are_input_types.py similarity index 87% rename from tests/core_validation/test_variables_are_input_types.py rename to graphql/validation/tests/test_variables_are_input_types.py index 55768d40..2810fed9 100644 --- a/tests/core_validation/test_variables_are_input_types.py +++ b/graphql/validation/tests/test_variables_are_input_types.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import VariablesAreInputTypes +from graphql.language.location import SourceLocation +from graphql.validation.rules import VariablesAreInputTypes + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/test_variables_in_allowed_position.py b/graphql/validation/tests/test_variables_in_allowed_position.py similarity index 98% rename from tests/core_validation/test_variables_in_allowed_position.py rename to graphql/validation/tests/test_variables_in_allowed_position.py index 4eaffeb3..716938d7 100644 --- a/tests/core_validation/test_variables_in_allowed_position.py +++ b/graphql/validation/tests/test_variables_in_allowed_position.py @@ -1,5 +1,6 @@ -from graphql.core.language.location import SourceLocation -from graphql.core.validation.rules import VariablesInAllowedPosition +from graphql.language.location import SourceLocation +from graphql.validation.rules import VariablesInAllowedPosition + from .utils import expect_fails_rule, expect_passes_rule diff --git a/tests/core_validation/utils.py b/graphql/validation/tests/utils.py similarity index 86% rename from tests/core_validation/utils.py rename to graphql/validation/tests/utils.py index 06fdec4c..42967929 100644 --- a/tests/core_validation/utils.py +++ b/graphql/validation/tests/utils.py @@ -1,16 +1,15 @@ -from graphql.core.error import format_error -from graphql.core.language.parser import parse -from graphql.core.type import (GraphQLArgument, GraphQLBoolean, - GraphQLEnumType, GraphQLEnumValue, GraphQLField, - GraphQLFloat, GraphQLID, - GraphQLInputObjectField, GraphQLInputObjectType, - GraphQLInt, GraphQLInterfaceType, GraphQLList, - GraphQLNonNull, GraphQLObjectType, - GraphQLSchema, GraphQLString, GraphQLUnionType) -from graphql.core.type.directives import (GraphQLDirective, - GraphQLIncludeDirective, - GraphQLSkipDirective) -from graphql.core.validation import validate +from graphql.error import format_error +from graphql.language.parser import parse +from graphql.type import (GraphQLArgument, GraphQLBoolean, GraphQLEnumType, + GraphQLEnumValue, GraphQLField, GraphQLFloat, + GraphQLID, GraphQLInputObjectField, + GraphQLInputObjectType, GraphQLInt, + GraphQLInterfaceType, GraphQLList, GraphQLNonNull, + GraphQLObjectType, GraphQLSchema, GraphQLString, + GraphQLUnionType) +from graphql.type.directives import (GraphQLDirective, GraphQLIncludeDirective, + GraphQLSkipDirective) +from graphql.validation import validate Being = GraphQLInterfaceType('Being', { 'name': GraphQLField(GraphQLString, { @@ -176,11 +175,15 @@ 'complicatedArgs': GraphQLField(ComplicatedArgs), }) -test_schema = GraphQLSchema(query=QueryRoot, directives=[ - GraphQLDirective(name='operationOnly', on_operation=True), - GraphQLIncludeDirective, - GraphQLSkipDirective -]) +test_schema = GraphQLSchema( + query=QueryRoot, + directives=[ + GraphQLDirective(name='operationOnly', locations=['QUERY']), + GraphQLIncludeDirective, + GraphQLSkipDirective + ], + types=[Cat, Dog, Human, Alien] +) def expect_valid(schema, rules, query): diff --git a/graphql/core/validation/context.py b/graphql/validation/validation.py similarity index 87% rename from graphql/core/validation/context.py rename to graphql/validation/validation.py index ebe8b202..cdfaebc8 100644 --- a/graphql/core/validation/context.py +++ b/graphql/validation/validation.py @@ -1,6 +1,24 @@ from ..language.ast import (FragmentDefinition, FragmentSpread, OperationDefinition) -from ..language.visitor import TypeInfoVisitor, Visitor, visit +from ..language.visitor import ParallelVisitor, TypeInfoVisitor, Visitor, visit +from ..type import GraphQLSchema +from ..utils.type_info import TypeInfo +from .rules import specified_rules + + +def validate(schema, ast, rules=specified_rules): + assert schema, 'Must provide schema' + assert ast, 'Must provide document' + assert isinstance(schema, GraphQLSchema) + type_info = TypeInfo(schema) + return visit_using_rules(schema, type_info, ast, rules) + + +def visit_using_rules(schema, type_info, ast, rules): + context = ValidationContext(schema, ast, type_info) + visitors = [rule(context) for rule in rules] + visit(ast, TypeInfoVisitor(type_info, ParallelVisitor(visitors))) + return context.get_errors() class VariableUsage(object): diff --git a/setup.py b/setup.py index 7312c562..e2613d11 100644 --- a/setup.py +++ b/setup.py @@ -27,8 +27,8 @@ keywords='api graphql protocol rest', packages=find_packages(exclude=['tests', 'tests_py35']), - install_requires=['six>=1.10.0'], - tests_require=['pytest>=2.7.3', 'gevent==1.1rc1', 'six>=1.10.0'], + install_requires=['six>=1.10.0','pypromise>=0.4.0'], + tests_require=['pytest>=2.7.3', 'gevent==1.1rc1', 'six>=1.10.0', 'pytest-mock'], extras_require={ 'gevent': [ 'gevent==1.1rc1' diff --git a/tests/core_execution/test_concurrent_executor.py b/tests/core_execution/test_concurrent_executor.py deleted file mode 100644 index d62dc559..00000000 --- a/tests/core_execution/test_concurrent_executor.py +++ /dev/null @@ -1,361 +0,0 @@ -from collections import OrderedDict - -from graphql.core.error import format_error -from graphql.core.execution import Executor -from graphql.core.execution.middlewares.sync import \ - SynchronousExecutionMiddleware -from graphql.core.pyutils.defer import Deferred, fail, succeed -from graphql.core.type import (GraphQLArgument, GraphQLField, GraphQLInt, - GraphQLList, GraphQLObjectType, GraphQLSchema, - GraphQLString) -from graphql.core.type.definition import GraphQLNonNull - -from .utils import raise_callback_results - - -def test_executes_arbitary_code(): - class Data(object): - a = 'Apple' - b = 'Banana' - c = 'Cookie' - d = 'Donut' - e = 'Egg' - - @property - def f(self): - return succeed('Fish') - - def pic(self, size=50): - return succeed('Pic of size: {}'.format(size)) - - def deep(self): - return DeepData() - - def promise(self): - return succeed(Data()) - - class DeepData(object): - a = 'Already Been Done' - b = 'Boring' - c = ['Contrived', None, succeed('Confusing')] - - def deeper(self): - return [Data(), None, succeed(Data())] - - doc = ''' - query Example($size: Int) { - a, - b, - x: c - ...c - f - ...on DataType { - pic(size: $size) - promise { - a - } - } - deep { - a - b - c - deeper { - a - b - } - } - } - fragment c on DataType { - d - e - } - ''' - - expected = { - 'a': 'Apple', - 'b': 'Banana', - 'x': 'Cookie', - 'd': 'Donut', - 'e': 'Egg', - 'f': 'Fish', - 'pic': 'Pic of size: 100', - 'promise': {'a': 'Apple'}, - 'deep': { - 'a': 'Already Been Done', - 'b': 'Boring', - 'c': ['Contrived', None, 'Confusing'], - 'deeper': [ - {'a': 'Apple', 'b': 'Banana'}, - None, - {'a': 'Apple', 'b': 'Banana'}]} - } - - DataType = GraphQLObjectType('DataType', lambda: { - 'a': GraphQLField(GraphQLString), - 'b': GraphQLField(GraphQLString), - 'c': GraphQLField(GraphQLString), - 'd': GraphQLField(GraphQLString), - 'e': GraphQLField(GraphQLString), - 'f': GraphQLField(GraphQLString), - 'pic': GraphQLField( - args={'size': GraphQLArgument(GraphQLInt)}, - type=GraphQLString, - resolver=lambda obj, args, *_: obj.pic(args['size']), - ), - 'deep': GraphQLField(DeepDataType), - 'promise': GraphQLField(DataType), - }) - - DeepDataType = GraphQLObjectType('DeepDataType', { - 'a': GraphQLField(GraphQLString), - 'b': GraphQLField(GraphQLString), - 'c': GraphQLField(GraphQLList(GraphQLString)), - 'deeper': GraphQLField(GraphQLList(DataType)), - }) - - schema = GraphQLSchema(query=DataType) - executor = Executor() - - def handle_result(result): - assert not result.errors - assert result.data == expected - - raise_callback_results(executor.execute(schema, doc, Data(), {'size': 100}, 'Example'), handle_result) - raise_callback_results(executor.execute(schema, doc, Data(), {'size': 100}, 'Example', execute_serially=True), - handle_result) - - -def test_synchronous_executor_doesnt_support_defers_with_nullable_type_getting_set_to_null(): - class Data(object): - - def promise(self): - return succeed('i shouldn\'nt work') - - def notPromise(self): - return 'i should work' - - DataType = GraphQLObjectType('DataType', { - 'promise': GraphQLField(GraphQLString), - 'notPromise': GraphQLField(GraphQLString), - }) - doc = ''' - query Example { - promise - notPromise - } - ''' - schema = GraphQLSchema(query=DataType) - executor = Executor([SynchronousExecutionMiddleware()]) - - result = executor.execute(schema, doc, Data(), operation_name='Example') - assert not isinstance(result, Deferred) - assert result.data == {"promise": None, 'notPromise': 'i should work'} - formatted_errors = list(map(format_error, result.errors)) - assert formatted_errors == [{'locations': [dict(line=3, column=9)], - 'message': 'You cannot return a Deferred from a resolver ' - 'when using SynchronousExecutionMiddleware'}] - - -def test_synchronous_executor_doesnt_support_defers(): - class Data(object): - - def promise(self): - return succeed('i shouldn\'nt work') - - def notPromise(self): - return 'i should work' - - DataType = GraphQLObjectType('DataType', { - 'promise': GraphQLField(GraphQLNonNull(GraphQLString)), - 'notPromise': GraphQLField(GraphQLString), - }) - doc = ''' - query Example { - promise - notPromise - } - ''' - schema = GraphQLSchema(query=DataType) - executor = Executor([SynchronousExecutionMiddleware()]) - - result = executor.execute(schema, doc, Data(), operation_name='Example') - assert not isinstance(result, Deferred) - assert result.data is None - formatted_errors = list(map(format_error, result.errors)) - assert formatted_errors == [{'locations': [dict(line=3, column=9)], - 'message': 'You cannot return a Deferred from a resolver ' - 'when using SynchronousExecutionMiddleware'}] - - -def test_executor_defer_failure(): - class Data(object): - - def promise(self): - return fail(Exception('Something bad happened! Sucks :(')) - - def notPromise(self): - return 'i should work' - - DataType = GraphQLObjectType('DataType', { - 'promise': GraphQLField(GraphQLNonNull(GraphQLString)), - 'notPromise': GraphQLField(GraphQLString), - }) - doc = ''' - query Example { - promise - notPromise - } - ''' - schema = GraphQLSchema(query=DataType) - executor = Executor() - - result = executor.execute(schema, doc, Data(), operation_name='Example') - assert result.called - result = result.result - assert result.data is None - formatted_errors = list(map(format_error, result.errors)) - assert formatted_errors == [{'locations': [dict(line=3, column=9)], - 'message': "Something bad happened! Sucks :("}] - - -def test_synchronous_executor_will_synchronously_resolve(): - class Data(object): - - def promise(self): - return 'I should work' - - DataType = GraphQLObjectType('DataType', { - 'promise': GraphQLField(GraphQLString), - }) - doc = ''' - query Example { - promise - } - ''' - schema = GraphQLSchema(query=DataType) - executor = Executor([SynchronousExecutionMiddleware()]) - - result = executor.execute(schema, doc, Data(), operation_name='Example') - assert not isinstance(result, Deferred) - assert result.data == {"promise": 'I should work'} - assert not result.errors - - -def test_synchronous_error_nulls_out_error_subtrees(): - doc = ''' - { - sync - syncError - syncReturnError - syncReturnErrorList - async - asyncReject - asyncEmptyReject - asyncReturnError - } - ''' - - class Data: - - def sync(self): - return 'sync' - - def syncError(self): - raise Exception('Error getting syncError') - - def syncReturnError(self): - return Exception("Error getting syncReturnError") - - def syncReturnErrorList(self): - return [ - 'sync0', - Exception('Error getting syncReturnErrorList1'), - 'sync2', - Exception('Error getting syncReturnErrorList3') - ] - - def async(self): - return succeed('async') - - def asyncReject(self): - return fail(Exception('Error getting asyncReject')) - - def asyncEmptyReject(self): - return fail() - - def asyncReturnError(self): - return succeed(Exception('Error getting asyncReturnError')) - - schema = GraphQLSchema( - query=GraphQLObjectType( - name='Type', - fields={ - 'sync': GraphQLField(GraphQLString), - 'syncError': GraphQLField(GraphQLString), - 'syncReturnError': GraphQLField(GraphQLString), - 'syncReturnErrorList': GraphQLField(GraphQLList(GraphQLString)), - 'async': GraphQLField(GraphQLString), - 'asyncReject': GraphQLField(GraphQLString), - 'asyncEmptyReject': GraphQLField(GraphQLString), - 'asyncReturnError': GraphQLField(GraphQLString), - } - ) - ) - - executor = Executor(map_type=OrderedDict) - - def handle_results(result): - assert result.data == { - 'async': 'async', - 'asyncEmptyReject': None, - 'asyncReject': None, - 'asyncReturnError': None, - 'sync': 'sync', - 'syncError': None, - 'syncReturnError': None, - 'syncReturnErrorList': ['sync0', None, 'sync2', None] - } - assert list(map(format_error, result.errors)) == [ - {'locations': [{'line': 4, 'column': 9}], 'message': 'Error getting syncError'}, - {'locations': [{'line': 5, 'column': 9}], 'message': 'Error getting syncReturnError'}, - {'locations': [{'line': 6, 'column': 9}], 'message': 'Error getting syncReturnErrorList1'}, - {'locations': [{'line': 6, 'column': 9}], 'message': 'Error getting syncReturnErrorList3'}, - {'locations': [{'line': 8, 'column': 9}], 'message': 'Error getting asyncReject'}, - {'locations': [{'line': 9, 'column': 9}], 'message': 'An unknown error occurred.'}, - {'locations': [{'line': 10, 'column': 9}], 'message': 'Error getting asyncReturnError'} - ] - - raise_callback_results(executor.execute(schema, doc, Data()), handle_results) - - -def test_executor_can_enforce_strict_ordering(): - Type = GraphQLObjectType('Type', lambda: { - 'a': GraphQLField(GraphQLString, - resolver=lambda *_: succeed('Apple')), - 'b': GraphQLField(GraphQLString, - resolver=lambda *_: succeed('Banana')), - 'c': GraphQLField(GraphQLString, - resolver=lambda *_: succeed('Cherry')), - 'deep': GraphQLField(Type, resolver=lambda *_: succeed({})), - }) - schema = GraphQLSchema(query=Type) - executor = Executor(map_type=OrderedDict) - - query = '{ a b c aa: c cc: c bb: b aaz: a bbz: b deep { b a c deeper: deep { c a b } } ' \ - 'ccz: c zzz: c aaa: a }' - - def handle_results(result): - assert not result.errors - - data = result.data - assert isinstance(data, OrderedDict) - assert list(data.keys()) == ['a', 'b', 'c', 'aa', 'cc', 'bb', 'aaz', 'bbz', 'deep', 'ccz', 'zzz', 'aaa'] - deep = data['deep'] - assert isinstance(deep, OrderedDict) - assert list(deep.keys()) == ['b', 'a', 'c', 'deeper'] - deeper = deep['deeper'] - assert isinstance(deeper, OrderedDict) - assert list(deeper.keys()) == ['c', 'a', 'b'] - - raise_callback_results(executor.execute(schema, query), handle_results) - raise_callback_results(executor.execute(schema, query, execute_serially=True), handle_results) diff --git a/tests/core_execution/test_default_executor.py b/tests/core_execution/test_default_executor.py deleted file mode 100644 index 20181f6d..00000000 --- a/tests/core_execution/test_default_executor.py +++ /dev/null @@ -1,17 +0,0 @@ -from graphql.core.execution import (Executor, get_default_executor, - set_default_executor) - - -def test_get_and_set_default_executor(): - e1 = get_default_executor() - e2 = get_default_executor() - assert e1 is e2 - - new_executor = Executor() - - set_default_executor(new_executor) - assert get_default_executor() is new_executor - - set_default_executor(None) - assert get_default_executor() is not e1 - assert get_default_executor() is not new_executor diff --git a/tests/core_execution/test_deferred.py b/tests/core_execution/test_deferred.py deleted file mode 100644 index 25032286..00000000 --- a/tests/core_execution/test_deferred.py +++ /dev/null @@ -1,278 +0,0 @@ -from pytest import raises - -from graphql.core.pyutils.defer import (AlreadyCalledDeferred, Deferred, - DeferredDict, DeferredException, - DeferredList, fail, succeed) - - -def test_succeed(): - d = succeed("123") - assert d.result == "123" - assert d.called - assert not d.callbacks - - -def test_fail_none(): - d = fail() - assert isinstance(d.result, DeferredException) - assert d.called - assert not d.callbacks - - -def test_fail_none_catches_exception(): - e = Exception('will be raised') - try: - raise e - except: - d = fail() - assert d.called - assert isinstance(d.result, DeferredException) - assert d.result.value == e - - -def test_fail(): - e = Exception('failed') - d = fail(e) - assert isinstance(d.result, DeferredException) - assert d.result.value == e - assert d.called - assert not d.callbacks - - -def test_nested_succeed(): - d = succeed(succeed('123')) - assert d.result == "123" - assert d.called - assert not d.callbacks - - d = succeed(succeed(succeed('123'))) - assert d.result == "123" - assert d.called - assert not d.callbacks - - -def test_callback_result_transformation(): - d = succeed(5) - d.add_callback(lambda r: r + 5) - assert d.result == 10 - - d.add_callback(lambda r: succeed(r + 5)) - - assert d.result == 15 - - -def test_deferred_list(): - d = Deferred() - - dl = DeferredList([ - 1, - d - ]) - - assert not dl.called - d.callback(2) - - assert dl.called - assert dl.result == [1, 2] - - -def test_deferred_list_with_already_resolved_deferred_values(): - dl = DeferredList([ - 1, - succeed(2), - succeed(3) - ]) - - assert dl.called - assert dl.result == [1, 2, 3] - - -def test_deferred_dict(): - d = Deferred() - - dd = DeferredDict({ - 'a': 1, - 'b': d - }) - - assert not dd.called - d.callback(2) - - assert dd.called - assert dd.result == {'a': 1, 'b': 2} - - -def test_deferred_list_of_no_defers(): - dl = DeferredList([ - {'ab': 1}, - 2, - [1, 2, 3], - "345" - ]) - - assert dl.called - assert dl.result == [ - {'ab': 1}, - 2, - [1, 2, 3], - "345" - ] - - -def test_callback_resolution(): - d = Deferred() - d.add_callback(lambda r: fail(Exception(r + "b"))) - d.add_errback(lambda e: e.value.args[0] + "c") - d.add_callbacks(lambda r: r + "d", lambda e: e.value.args[0] + 'f') - - d.callback("a") - - assert d.result == "abcd" - - -def test_callback_resolution_weaving(): - d = Deferred() - d.add_callbacks(lambda r: fail(Exception(r + "b")), lambda e: e.value.args[0] + 'w') - d.add_callbacks(lambda e: Exception(e + "x"), lambda e: e.value.args[0] + "c") - d.add_callbacks(lambda r: Exception(r + "d"), lambda e: e.value.args[0] + 'y') - d.add_callbacks(lambda r: r + "z", lambda e: e.value.args[0] + 'e') - - d.callback("a") - - assert d.result == "abcde" - - -def test_callback_resolution_weaving_2(): - d = Deferred() - d.add_callbacks(lambda r: fail(Exception(r + "b")), lambda e: e.value.args[0] + 'w') - d.add_callbacks(lambda e: Exception(e + "x"), lambda e: e.value.args[0] + "c") - d.add_callbacks(lambda r: Exception(r + "d"), lambda e: e.value.args[0] + 'y') - d.add_callbacks(lambda r: fail(ValueError(r + "z")), lambda e: e.value.args[0] + 'e') - - d.errback(Exception('v')) - - assert isinstance(d.result, DeferredException) - assert isinstance(d.result.value, ValueError) - assert d.result.value.args[0] == "vwxyz" - - -def test_callback_raises_exception(): - def callback(val): - raise AttributeError(val) - - d = Deferred() - d.add_callback(callback) - d.callback('test') - - assert isinstance(d.result, DeferredException) - assert isinstance(d.result.value, AttributeError) - assert d.result.value.args[0] == "test" - - -def test_errback(): - holder = [] - d = Deferred() - e = Exception('errback test') - d.add_errback(lambda e: holder.append(e)) - d.errback(e) - - assert isinstance(holder[0], DeferredException) - assert holder[0].value == e - - -def test_errback_chain(): - holder = [] - d = Deferred() - e = Exception('a') - d.add_callbacks(holder.append, lambda e: Exception(e.value.args[0] + 'b')) - d.add_callbacks(holder.append, lambda e: Exception(e.value.args[0] + 'c')) - - d.errback(e) - - assert d.result.value.args[0] == 'abc' - assert len(holder) == 0 - - -def test_deferred_list_fails(): - d1 = Deferred() - d2 = Deferred() - d3 = Deferred() - - dl = DeferredList([ - 1, - succeed(2), - d1, - d2, - d3 - ]) - - assert not dl.called - - e1 = Exception('d1 failed') - d1.errback(e1) - d2.errback(Exception('d2 failed')) - d3.callback('hello') - - assert dl.called - assert isinstance(dl.result, DeferredException) - assert dl.result.value == e1 - - -def test_cant_callback_twice(): - d1 = Deferred() - d1.callback('hello') - - with raises(AlreadyCalledDeferred): - d1.callback('world') - - -def test_cant_errback_twice(): - d1 = Deferred() - d1.errback(Exception('hello')) - - with raises(AlreadyCalledDeferred): - d1.errback(Exception('world')) - - -def test_callbacks_and_errbacks_return_original_deferred(): - d = Deferred() - assert d.add_callback(lambda a: None) is d - assert d.add_errback(lambda a: None) is d - assert d.add_callbacks(lambda a: None, lambda a: None) is d - - -def test_callback_var_args(): - holder = [] - d = Deferred() - d.add_callback(lambda *args, **kwargs: holder.append((args, kwargs)), 2, 3, a=4, b=5) - d.callback(1) - - assert holder[0] == ((1, 2, 3), {'a': 4, 'b': 5}) - - -def test_deferred_callback_returns_another_deferred(): - d = Deferred() - d2 = Deferred() - - d.add_callback(lambda r: succeed(r + 5).add_callback(lambda v: v + 5)) - d.add_callback(lambda r: d2) - d.callback(5) - - assert d.result is d2 - assert d.paused - assert d.called - - d2.callback(7) - assert d.result == 7 - assert d2.result == 7 - - -def test_deferred_exception_catch(): - def dummy_errback(deferred_exception): - deferred_exception.catch(OSError) - return "caught" - - deferred = Deferred() - deferred.add_errback(dummy_errback) - deferred.errback(OSError()) - assert deferred.result == 'caught' diff --git a/tests/core_execution/test_middleware.py b/tests/core_execution/test_middleware.py deleted file mode 100644 index 806e0ed3..00000000 --- a/tests/core_execution/test_middleware.py +++ /dev/null @@ -1,51 +0,0 @@ -from graphql.core.execution.middlewares.utils import (merge_resolver_tags, - resolver_has_tag, - tag_resolver) - - -def test_tag_resolver(): - resolver = lambda: None - - tag_resolver(resolver, 'test') - assert resolver_has_tag(resolver, 'test') - assert not resolver_has_tag(resolver, 'not test') - - -def test_merge_resolver_tags(): - a = lambda: None - b = lambda: None - - tag_resolver(a, 'a') - tag_resolver(b, 'b') - - merge_resolver_tags(a, b) - - assert resolver_has_tag(a, 'a') - assert not resolver_has_tag(a, 'b') - - assert resolver_has_tag(b, 'a') - assert resolver_has_tag(b, 'b') - - -def test_resolver_has_tag_with_untagged_resolver(): - a = lambda: None - - assert not resolver_has_tag(a, 'anything') - - -def test_merge_resolver_from_untagged_source(): - a = lambda: None - b = lambda: None - - merge_resolver_tags(a, b) - assert not hasattr(b, '_resolver_tags') - - -def test_merge_resolver_to_untagged_target(): - a = lambda: None - b = lambda: None - - tag_resolver(a, 'test') - merge_resolver_tags(a, b) - - assert resolver_has_tag(b, 'test') diff --git a/tests/core_execution/utils.py b/tests/core_execution/utils.py deleted file mode 100644 index b87d3471..00000000 --- a/tests/core_execution/utils.py +++ /dev/null @@ -1,43 +0,0 @@ -from graphql.core.pyutils.defer import (Deferred, DeferredException, - _passthrough) - - -class RaisingDeferred(Deferred): - - def _next(self): - """Process the next callback.""" - if self._running or self.paused: - return - - while self.callbacks: - # Get the next callback pair - next_pair = self.callbacks.pop(0) - # Continue with the errback if the last result was an exception - callback, args, kwargs = next_pair[isinstance(self.result, - DeferredException)] - - if callback is not _passthrough: - self._running = True - try: - self.result = callback(self.result, *args, **kwargs) - - except: - self.result = DeferredException() - - finally: - self._running = False - - if isinstance(self.result, Exception): - self.result = DeferredException(self.result) - - if isinstance(self.result, DeferredException): - # Print the exception to stderr and stop if there aren't any - # further errbacks to process - self.result.raise_exception() - - -def raise_callback_results(deferred, callback): - d = RaisingDeferred() - d.add_callback(lambda r: r) - d.callback(deferred) - d.add_callback(callback) diff --git a/tests/core_type/test_introspection.py b/tests/core_type/test_introspection.py deleted file mode 100644 index 4444b4ac..00000000 --- a/tests/core_type/test_introspection.py +++ /dev/null @@ -1,831 +0,0 @@ -import json -from collections import OrderedDict - -from graphql.core import graphql -from graphql.core.error import format_error -from graphql.core.execution import execute -from graphql.core.language.parser import parse -from graphql.core.type import (GraphQLArgument, GraphQLEnumType, - GraphQLEnumValue, GraphQLField, - GraphQLInputObjectField, GraphQLInputObjectType, - GraphQLList, GraphQLObjectType, GraphQLSchema, - GraphQLString) -from graphql.core.utils.introspection_query import introspection_query -from graphql.core.validation.rules import ProvidedNonNullArguments - - -def test_executes_an_introspection_query(): - EmptySchema = GraphQLSchema(GraphQLObjectType('QueryRoot', {'f': GraphQLField(GraphQLString)})) - - result = graphql(EmptySchema, introspection_query) - assert not result.errors - expected = { - '__schema': {'directives': [{'args': [{'defaultValue': None, - 'description': u'Directs the executor to include this field or fragment only when the `if` argument is true.', - 'name': u'if', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'SCALAR', - 'name': u'Boolean', - 'ofType': None}}}], - 'description': None, - 'name': u'include', - 'onField': True, - 'onFragment': True, - 'onOperation': False}, - {'args': [{'defaultValue': None, - 'description': u'Directs the executor to skip this field or fragment only when the `if` argument is true.', - 'name': u'if', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'SCALAR', - 'name': u'Boolean', - 'ofType': None}}}], - 'description': None, - 'name': u'skip', - 'onField': True, - 'onFragment': True, - 'onOperation': False}], - 'mutationType': None, - 'queryType': {'name': u'QueryRoot'}, - 'subscriptionType': None, - 'types': [{'description': None, - 'enumValues': None, - 'fields': [{'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'f', - 'type': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}], - 'inputFields': None, - 'interfaces': [], - 'kind': 'OBJECT', - 'name': u'QueryRoot', - 'possibleTypes': None}, - { - 'description': u'The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.', - 'enumValues': None, - 'fields': None, - 'inputFields': None, - 'interfaces': None, - 'kind': 'SCALAR', - 'name': u'String', - 'possibleTypes': None}, - { - 'description': u'A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation and subscription operations.', - 'enumValues': None, - 'fields': [{'args': [], - 'deprecationReason': None, - 'description': u'A list of all types supported by this server.', - 'isDeprecated': False, - 'name': u'types', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'LIST', - 'name': None, - 'ofType': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'OBJECT', - 'name': u'__Type'}}}}}, - {'args': [], - 'deprecationReason': None, - 'description': u'The type that query operations will be rooted at.', - 'isDeprecated': False, - 'name': u'queryType', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'OBJECT', - 'name': u'__Type', - 'ofType': None}}}, - {'args': [], - 'deprecationReason': None, - 'description': u'If this server supports mutation, the type that mutation operations will be rooted at.', - 'isDeprecated': False, - 'name': u'mutationType', - 'type': {'kind': 'OBJECT', - 'name': u'__Type', - 'ofType': None}}, - {'args': [], - 'deprecationReason': None, - 'description': u'If this server support subscription, the type that subscription operations will be rooted at.', - 'isDeprecated': False, - 'name': u'subscriptionType', - 'type': {'kind': 'OBJECT', - 'name': u'__Type', - 'ofType': None}}, - {'args': [], - 'deprecationReason': None, - 'description': u'A list of all directives supported by this server.', - 'isDeprecated': False, - 'name': u'directives', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'LIST', - 'name': None, - 'ofType': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'OBJECT', - 'name': u'__Directive'}}}}}], - 'inputFields': None, - 'interfaces': [], - 'kind': 'OBJECT', - 'name': u'__Schema', - 'possibleTypes': None}, - { - 'description': u'The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.', - 'enumValues': None, - 'fields': [{'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'kind', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'ENUM', - 'name': u'__TypeKind', - 'ofType': None}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'name', - 'type': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'description', - 'type': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}, - {'args': [{'defaultValue': u'false', - 'description': None, - 'name': u'includeDeprecated', - 'type': {'kind': 'SCALAR', - 'name': u'Boolean', - 'ofType': None}}], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'fields', - 'type': {'kind': 'LIST', - 'name': None, - 'ofType': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'OBJECT', - 'name': u'__Field', - 'ofType': None}}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'interfaces', - 'type': {'kind': 'LIST', - 'name': None, - 'ofType': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'OBJECT', - 'name': u'__Type', - 'ofType': None}}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'possibleTypes', - 'type': {'kind': 'LIST', - 'name': None, - 'ofType': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'OBJECT', - 'name': u'__Type', - 'ofType': None}}}}, - {'args': [{'defaultValue': u'false', - 'description': None, - 'name': u'includeDeprecated', - 'type': {'kind': 'SCALAR', - 'name': u'Boolean', - 'ofType': None}}], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'enumValues', - 'type': {'kind': 'LIST', - 'name': None, - 'ofType': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'OBJECT', - 'name': u'__EnumValue', - 'ofType': None}}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'inputFields', - 'type': {'kind': 'LIST', - 'name': None, - 'ofType': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'OBJECT', - 'name': u'__InputValue', - 'ofType': None}}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'ofType', - 'type': {'kind': 'OBJECT', - 'name': u'__Type', - 'ofType': None}}], - 'inputFields': None, - 'interfaces': [], - 'kind': 'OBJECT', - 'name': u'__Type', - 'possibleTypes': None}, - {'description': u'An enum describing what kind of type a given `__Type` is', - 'enumValues': [{'deprecationReason': None, - 'description': u'Indicates this type is a scalar.', - 'isDeprecated': False, - 'name': u'SCALAR'}, - {'deprecationReason': None, - 'description': u'Indicates this type is an object. `fields` and `interfaces` are valid fields.', - 'isDeprecated': False, - 'name': u'OBJECT'}, - {'deprecationReason': None, - 'description': u'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.', - 'isDeprecated': False, - 'name': u'INTERFACE'}, - {'deprecationReason': None, - 'description': u'Indicates this type is a union. `possibleTypes` is a valid field.', - 'isDeprecated': False, - 'name': u'UNION'}, - {'deprecationReason': None, - 'description': u'Indicates this type is an enum. `enumValues` is a valid field.', - 'isDeprecated': False, - 'name': u'ENUM'}, - {'deprecationReason': None, - 'description': u'Indicates this type is an input object. `inputFields` is a valid field.', - 'isDeprecated': False, - 'name': u'INPUT_OBJECT'}, - {'deprecationReason': None, - 'description': u'Indicates this type is a list. `ofType` is a valid field.', - 'isDeprecated': False, - 'name': u'LIST'}, - {'deprecationReason': None, - 'description': u'Indicates this type is a non-null. `ofType` is a valid field.', - 'isDeprecated': False, - 'name': u'NON_NULL'}], - 'fields': None, - 'inputFields': None, - 'interfaces': None, - 'kind': 'ENUM', - 'name': u'__TypeKind', - 'possibleTypes': None}, - {'description': u'The `Boolean` scalar type represents `true` or `false`.', - 'enumValues': None, - 'fields': None, - 'inputFields': None, - 'interfaces': None, - 'kind': 'SCALAR', - 'name': u'Boolean', - 'possibleTypes': None}, - { - 'description': u'Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.', - 'enumValues': None, - 'fields': [{'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'name', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'description', - 'type': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'args', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'LIST', - 'name': None, - 'ofType': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'OBJECT', - 'name': u'__InputValue'}}}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'type', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'OBJECT', - 'name': u'__Type', - 'ofType': None}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'isDeprecated', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'SCALAR', - 'name': u'Boolean', - 'ofType': None}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'deprecationReason', - 'type': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}], - 'inputFields': None, - 'interfaces': [], - 'kind': 'OBJECT', - 'name': u'__Field', - 'possibleTypes': None}, - { - 'description': u'Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.', - 'enumValues': None, - 'fields': [{'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'name', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'description', - 'type': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'type', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'OBJECT', - 'name': u'__Type', - 'ofType': None}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'defaultValue', - 'type': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}], - 'inputFields': None, - 'interfaces': [], - 'kind': 'OBJECT', - 'name': u'__InputValue', - 'possibleTypes': None}, - { - 'description': u'One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.', - 'enumValues': None, - 'fields': [{'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'name', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'description', - 'type': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'isDeprecated', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'SCALAR', - 'name': u'Boolean', - 'ofType': None}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'deprecationReason', - 'type': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}], - 'inputFields': None, - 'interfaces': [], - 'kind': 'OBJECT', - 'name': u'__EnumValue', - 'possibleTypes': None}, - { - 'description': u"A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", - 'enumValues': None, - 'fields': [{'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'name', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'description', - 'type': {'kind': 'SCALAR', - 'name': u'String', - 'ofType': None}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'args', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'LIST', - 'name': None, - 'ofType': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'OBJECT', - 'name': u'__InputValue'}}}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'onOperation', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'SCALAR', - 'name': u'Boolean', - 'ofType': None}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'onFragment', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'SCALAR', - 'name': u'Boolean', - 'ofType': None}}}, - {'args': [], - 'deprecationReason': None, - 'description': None, - 'isDeprecated': False, - 'name': u'onField', - 'type': {'kind': 'NON_NULL', - 'name': None, - 'ofType': {'kind': 'SCALAR', - 'name': u'Boolean', - 'ofType': None}}}], - 'inputFields': None, - 'interfaces': [], - 'kind': 'OBJECT', - 'name': u'__Directive', - 'possibleTypes': None}]}} - assert result.data == expected - - -def test_introspects_on_input_object(): - TestInputObject = GraphQLInputObjectType('TestInputObject', OrderedDict([ - ('a', GraphQLInputObjectField(GraphQLString, default_value='foo')), - ('b', GraphQLInputObjectField(GraphQLList(GraphQLString))) - ])) - TestType = GraphQLObjectType('TestType', { - 'field': GraphQLField( - type=GraphQLString, - args={'complex': GraphQLArgument(TestInputObject)}, - resolver=lambda obj, args, info: json.dumps(args.get('complex')) - ) - }) - schema = GraphQLSchema(TestType) - request = ''' - { - __schema { - types { - kind - name - inputFields { - name - type { ...TypeRef } - defaultValue - } - } - } - } - fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - } - ''' - result = graphql(schema, request) - assert not result.errors - assert {'kind': 'INPUT_OBJECT', - 'name': 'TestInputObject', - 'inputFields': - [{'name': 'a', - 'type': - {'kind': 'SCALAR', - 'name': 'String', - 'ofType': None}, - 'defaultValue': '"foo"'}, - {'name': 'b', - 'type': - {'kind': 'LIST', - 'name': None, - 'ofType': - {'kind': 'SCALAR', - 'name': 'String', - 'ofType': None}}, - 'defaultValue': None}]} in result.data['__schema']['types'] - - -def test_supports_the_type_root_field(): - TestType = GraphQLObjectType('TestType', { - 'testField': GraphQLField(GraphQLString) - }) - schema = GraphQLSchema(TestType) - request = '{ __type(name: "TestType") { name } }' - result = execute(schema, object(), parse(request)) - assert not result.errors - assert result.data == {'__type': {'name': 'TestType'}} - - -def test_identifies_deprecated_fields(): - TestType = GraphQLObjectType('TestType', OrderedDict([ - ('nonDeprecated', GraphQLField(GraphQLString)), - ('deprecated', GraphQLField(GraphQLString, deprecation_reason='Removed in 1.0')) - ])) - schema = GraphQLSchema(TestType) - request = '''{__type(name: "TestType") { - name - fields(includeDeprecated: true) { - name - isDeprecated - deprecationReason - } - } }''' - result = graphql(schema, request) - assert not result.errors - assert result.data == {'__type': { - 'name': 'TestType', - 'fields': [ - {'name': 'nonDeprecated', 'isDeprecated': False, 'deprecationReason': None}, - {'name': 'deprecated', 'isDeprecated': True, - 'deprecationReason': 'Removed in 1.0'}, - ] - }} - - -def test_respects_the_includedeprecated_parameter_for_fields(): - TestType = GraphQLObjectType('TestType', OrderedDict([ - ('nonDeprecated', GraphQLField(GraphQLString)), - ('deprecated', GraphQLField(GraphQLString, deprecation_reason='Removed in 1.0')) - ])) - schema = GraphQLSchema(TestType) - request = '''{__type(name: "TestType") { - name - trueFields: fields(includeDeprecated: true) { name } - falseFields: fields(includeDeprecated: false) { name } - omittedFields: fields { name } - } }''' - result = graphql(schema, request) - assert not result.errors - assert result.data == {'__type': { - 'name': 'TestType', - 'trueFields': [{'name': 'nonDeprecated'}, {'name': 'deprecated'}], - 'falseFields': [{'name': 'nonDeprecated'}], - 'omittedFields': [{'name': 'nonDeprecated'}], - }} - - -def test_identifies_deprecated_enum_values(): - TestEnum = GraphQLEnumType('TestEnum', OrderedDict([ - ('NONDEPRECATED', GraphQLEnumValue(0)), - ('DEPRECATED', GraphQLEnumValue(1, deprecation_reason='Removed in 1.0')), - ('ALSONONDEPRECATED', GraphQLEnumValue(2)) - ])) - TestType = GraphQLObjectType('TestType', { - 'testEnum': GraphQLField(TestEnum) - }) - schema = GraphQLSchema(TestType) - request = '''{__type(name: "TestEnum") { - name - enumValues(includeDeprecated: true) { - name - isDeprecated - deprecationReason - } - } }''' - result = graphql(schema, request) - assert not result.errors - assert result.data == {'__type': { - 'name': 'TestEnum', - 'enumValues': [ - {'name': 'NONDEPRECATED', 'isDeprecated': False, 'deprecationReason': None}, - {'name': 'DEPRECATED', 'isDeprecated': True, 'deprecationReason': 'Removed in 1.0'}, - {'name': 'ALSONONDEPRECATED', 'isDeprecated': False, 'deprecationReason': None}, - ]}} - - -def test_respects_the_includedeprecated_parameter_for_enum_values(): - TestEnum = GraphQLEnumType('TestEnum', OrderedDict([ - ('NONDEPRECATED', GraphQLEnumValue(0)), - ('DEPRECATED', GraphQLEnumValue(1, deprecation_reason='Removed in 1.0')), - ('ALSONONDEPRECATED', GraphQLEnumValue(2)) - ])) - TestType = GraphQLObjectType('TestType', { - 'testEnum': GraphQLField(TestEnum) - }) - schema = GraphQLSchema(TestType) - request = '''{__type(name: "TestEnum") { - name - trueValues: enumValues(includeDeprecated: true) { name } - falseValues: enumValues(includeDeprecated: false) { name } - omittedValues: enumValues { name } - } }''' - result = graphql(schema, request) - assert not result.errors - assert result.data == {'__type': { - 'name': 'TestEnum', - 'trueValues': [{'name': 'NONDEPRECATED'}, {'name': 'DEPRECATED'}, - {'name': 'ALSONONDEPRECATED'}], - 'falseValues': [{'name': 'NONDEPRECATED'}, - {'name': 'ALSONONDEPRECATED'}], - 'omittedValues': [{'name': 'NONDEPRECATED'}, - {'name': 'ALSONONDEPRECATED'}], - }} - - -def test_fails_as_expected_on_the_type_root_field_without_an_arg(): - TestType = GraphQLObjectType('TestType', { - 'testField': GraphQLField(GraphQLString) - }) - schema = GraphQLSchema(TestType) - request = ''' - { - __type { - name - } - }''' - result = graphql(schema, request) - expected_error = {'message': ProvidedNonNullArguments.missing_field_arg_message('__type', 'name', 'String!'), - 'locations': [dict(line=3, column=9)]} - assert (expected_error in [format_error(error) for error in result.errors]) - - -def test_exposes_descriptions_on_types_and_fields(): - QueryRoot = GraphQLObjectType('QueryRoot', {'f': GraphQLField(GraphQLString)}) - schema = GraphQLSchema(QueryRoot) - request = '''{ - schemaType: __type(name: "__Schema") { - name, - description, - fields { - name, - description - } - } - } - ''' - result = graphql(schema, request) - assert not result.errors - assert result.data == {'schemaType': { - 'name': '__Schema', - 'description': 'A GraphQL Schema defines the capabilities of a ' + - 'GraphQL server. It exposes all available types and ' + - 'directives on the server, as well as the entry ' + - 'points for query, mutation and subscription operations.', - 'fields': [ - { - 'name': 'types', - 'description': 'A list of all types supported by this server.' - }, - { - 'name': 'queryType', - 'description': 'The type that query operations will be rooted at.' - }, - { - 'name': 'mutationType', - 'description': 'If this server supports mutation, the type that ' - 'mutation operations will be rooted at.' - }, - { - 'name': 'subscriptionType', - 'description': 'If this server support subscription, the type ' - 'that subscription operations will be rooted at.' - }, - { - 'name': 'directives', - 'description': 'A list of all directives supported by this server.' - } - ] - }} - - -def test_exposes_descriptions_on_enums(): - QueryRoot = GraphQLObjectType('QueryRoot', {'f': GraphQLField(GraphQLString)}) - schema = GraphQLSchema(QueryRoot) - request = '''{ - typeKindType: __type(name: "__TypeKind") { - name, - description, - enumValues { - name, - description - } - } - } - ''' - result = graphql(schema, request) - assert not result.errors - assert result.data == {'typeKindType': { - 'name': '__TypeKind', - 'description': 'An enum describing what kind of type a given `__Type` is', - 'enumValues': [ - { - 'description': 'Indicates this type is a scalar.', - 'name': 'SCALAR' - }, - { - 'description': 'Indicates this type is an object. ' + - '`fields` and `interfaces` are valid fields.', - 'name': 'OBJECT' - }, - { - 'description': 'Indicates this type is an interface. ' + - '`fields` and `possibleTypes` are valid fields.', - 'name': 'INTERFACE' - }, - { - 'description': 'Indicates this type is a union. ' + - '`possibleTypes` is a valid field.', - 'name': 'UNION' - }, - { - 'description': 'Indicates this type is an enum. ' + - '`enumValues` is a valid field.', - 'name': 'ENUM' - }, - { - 'description': 'Indicates this type is an input object. ' + - '`inputFields` is a valid field.', - 'name': 'INPUT_OBJECT' - }, - { - 'description': 'Indicates this type is a list. ' + - '`ofType` is a valid field.', - 'name': 'LIST' - }, - { - 'description': 'Indicates this type is a non-null. ' + - '`ofType` is a valid field.', - 'name': 'NON_NULL' - } - ] - }} diff --git a/tests/core_utils/test_build_ast_schema.py b/tests/core_utils/test_build_ast_schema.py deleted file mode 100644 index 8d9fd308..00000000 --- a/tests/core_utils/test_build_ast_schema.py +++ /dev/null @@ -1,375 +0,0 @@ -from pytest import raises - -from graphql.core import parse -from graphql.core.utils.build_ast_schema import build_ast_schema -from graphql.core.utils.schema_printer import print_schema - - -def cycle_output(body, query_type, mutation_type=None, subscription_type=None): - ast = parse(body) - schema = build_ast_schema(ast, query_type, mutation_type, subscription_type) - return '\n' + print_schema(schema) - - -def test_simple_type(): - body = ''' -type HelloScalars { - str: String - int: Int - float: Float - id: ID - bool: Boolean -} -''' - output = cycle_output(body, 'HelloScalars') - assert output == body - - -def test_type_modifiers(): - body = ''' -type HelloScalars { - nonNullStr: String! - listOfStrs: [String] - listOfNonNullStrs: [String!] - nonNullListOfStrs: [String]! - nonNullListOfNonNullStrs: [String!]! -} -''' - output = cycle_output(body, 'HelloScalars') - assert output == body - - -def test_recursive_type(): - body = ''' -type Recurse { - str: String - recurse: Recurse -} -''' - output = cycle_output(body, 'Recurse') - assert output == body - - -def test_two_types_circular(): - body = ''' -type TypeOne { - str: String - typeTwo: TypeTwo -} - -type TypeTwo { - str: String - typeOne: TypeOne -} -''' - output = cycle_output(body, 'TypeOne') - assert output == body - - -def test_single_argument_field(): - body = ''' -type Hello { - str(int: Int): String - floatToStr(float: Float): String - idToStr(id: ID): String - booleanToStr(bool: Boolean): String - strToStr(bool: String): String -} -''' - output = cycle_output(body, 'Hello') - assert output == body - - -def test_simple_type_with_multiple_arguments(): - body = ''' -type Hello { - str(int: Int, bool: Boolean): String -} -''' - output = cycle_output(body, 'Hello') - assert output == body - - -def test_simple_type_with_interface(): - body = ''' -type HelloInterface implements WorldInterface { - str: String -} - -interface WorldInterface { - str: String -} -''' - output = cycle_output(body, 'HelloInterface') - assert output == body - - -def test_simple_output_enum(): - body = ''' -enum Hello { - WORLD -} - -type OutputEnumRoot { - hello: Hello -} -''' - output = cycle_output(body, 'OutputEnumRoot') - assert output == body - - -def test_simple_input_enum(): - body = ''' -enum Hello { - WORLD -} - -type InputEnumRoot { - str(hello: Hello): String -} -''' - output = cycle_output(body, 'InputEnumRoot') - assert output == body - - -def test_multiple_value_enum(): - body = ''' -enum Hello { - WO - RLD -} - -type OutputEnumRoot { - hello: Hello -} -''' - output = cycle_output(body, 'OutputEnumRoot') - assert output == body - - -def test_simple_union(): - body = ''' -union Hello = World - -type Root { - hello: Hello -} - -type World { - str: String -} -''' - output = cycle_output(body, 'Root') - assert output == body - - -def test_multiple_union(): - body = ''' -union Hello = WorldOne | WorldTwo - -type Root { - hello: Hello -} - -type WorldOne { - str: String -} - -type WorldTwo { - str: String -} -''' - output = cycle_output(body, 'Root') - assert output == body - - -def test_custom_scalar(): - body = ''' -scalar CustomScalar - -type Root { - customScalar: CustomScalar -} -''' - output = cycle_output(body, 'Root') - assert output == body - - -def test_input_object(): - body = ''' -input Input { - int: Int -} - -type Root { - field(in: Input): String -} -''' - output = cycle_output(body, 'Root') - assert output == body - - -def test_simple_argument_field_with_default(): - body = ''' -type Hello { - str(int: Int = 2): String -} -''' - output = cycle_output(body, 'Hello') - assert output == body - - -def test_simple_type_with_mutation(): - body = ''' -type HelloScalars { - str: String - int: Int - bool: Boolean -} - -type Mutation { - addHelloScalars(str: String, int: Int, bool: Boolean): HelloScalars -} -''' - output = cycle_output(body, 'HelloScalars', 'Mutation') - assert output == body - - -def test_simple_type_with_subscription(): - body = ''' -type HelloScalars { - str: String - int: Int - bool: Boolean -} - -type Subscription { - subscribeHelloScalars(str: String, int: Int, bool: Boolean): HelloScalars -} -''' - output = cycle_output(body, 'HelloScalars', None, 'Subscription') - assert output == body - - -def test_unreferenced_type_implementing_referenced_interface(): - body = ''' -type Concrete implements Iface { - key: String -} - -interface Iface { - key: String -} - -type Query { - iface: Iface -} -''' - output = cycle_output(body, 'Query') - assert output == body - - -def test_unreferenced_type_implementing_referenced_union(): - body = ''' -type Concrete { - key: String -} - -type Query { - union: Union -} - -union Union = Concrete -''' - output = cycle_output(body, 'Query') - assert output == body - - -def test_unknown_type_referenced(): - body = ''' -type Hello { - bar: Bar -} -''' - doc = parse(body) - with raises(Exception) as excinfo: - build_ast_schema(doc, 'Hello') - - assert 'Type Bar not found in document' in str(excinfo.value) - - -def test_unknown_type_in_union_list(): - body = ''' -union TestUnion = Bar -type Hello { testUnion: TestUnion } -''' - doc = parse(body) - with raises(Exception) as excinfo: - build_ast_schema(doc, 'Hello') - - assert 'Type Bar not found in document' in str(excinfo.value) - - -def test_unknown_query_type(): - body = ''' -type Hello { - str: String -} -''' - doc = parse(body) - with raises(Exception) as excinfo: - build_ast_schema(doc, 'Wat') - - assert 'Specified query type Wat not found in document' in str(excinfo.value) - - -def test_unknown_mutation_type(): - body = ''' -type Hello { - str: String -} -''' - doc = parse(body) - with raises(Exception) as excinfo: - build_ast_schema(doc, 'Hello', 'Wat') - - assert 'Specified mutation type Wat not found in document' in str(excinfo.value) - - -def test_unknown_subscription_type(): - body = ''' -type Hello { - str: String -} - -type Wat { - str: String -} -''' - doc = parse(body) - with raises(Exception) as excinfo: - build_ast_schema(doc, 'Hello', 'Wat', 'Awesome') - - assert 'Specified subscription type Awesome not found in document' in str(excinfo.value) - - -def test_rejects_query_names(): - body = ''' -type Hello { - str: String -} -''' - doc = parse(body) - with raises(Exception) as excinfo: - build_ast_schema(doc, 'Foo') - - assert 'Specified query type Foo not found in document' in str(excinfo.value) - - -def test_rejects_fragment_names(): - body = '''fragment Foo on Type { field } ''' - doc = parse(body) - with raises(Exception) as excinfo: - build_ast_schema(doc, 'Foo') - - assert 'Specified query type Foo not found in document' in str(excinfo.value) diff --git a/tests/starwars/__init__.py b/tests/starwars/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/core_starwars/starwars_fixtures.py b/tests/starwars/starwars_fixtures.py similarity index 100% rename from tests/core_starwars/starwars_fixtures.py rename to tests/starwars/starwars_fixtures.py diff --git a/tests/core_starwars/starwars_schema.py b/tests/starwars/starwars_schema.py similarity index 92% rename from tests/core_starwars/starwars_schema.py rename to tests/starwars/starwars_schema.py index 9ca43fed..5e875401 100644 --- a/tests/core_starwars/starwars_schema.py +++ b/tests/starwars/starwars_schema.py @@ -1,8 +1,7 @@ -from graphql.core.type import (GraphQLArgument, GraphQLEnumType, - GraphQLEnumValue, GraphQLField, - GraphQLInterfaceType, GraphQLList, - GraphQLNonNull, GraphQLObjectType, - GraphQLSchema, GraphQLString) +from graphql.type import (GraphQLArgument, GraphQLEnumType, GraphQLEnumValue, + GraphQLField, GraphQLInterfaceType, GraphQLList, + GraphQLNonNull, GraphQLObjectType, GraphQLSchema, + GraphQLString) from .starwars_fixtures import getDroid, getFriends, getHero, getHuman @@ -144,4 +143,4 @@ } ) -StarWarsSchema = GraphQLSchema(query=queryType) +StarWarsSchema = GraphQLSchema(query=queryType, types=[humanType, droidType]) diff --git a/tests/starwars/test_introspection.py b/tests/starwars/test_introspection.py new file mode 100644 index 00000000..7cac8a20 --- /dev/null +++ b/tests/starwars/test_introspection.py @@ -0,0 +1,414 @@ +from graphql import graphql +from graphql.error import format_error +from graphql.pyutils.contain_subset import contain_subset + +from .starwars_schema import StarWarsSchema + + +def test_allows_querying_the_schema_for_types(): + query = ''' + query IntrospectionTypeQuery { + __schema { + types { + name + } + } + } + ''' + expected = { + "__schema": { + "types": [ + { + "name": 'Query' + }, + { + "name": 'Episode' + }, + { + "name": 'Character' + }, + { + "name": 'String' + }, + { + "name": 'Human' + }, + { + "name": 'Droid' + }, + { + "name": '__Schema' + }, + { + "name": '__Type' + }, + { + "name": '__TypeKind' + }, + { + "name": 'Boolean' + }, + { + "name": '__Field' + }, + { + "name": '__InputValue' + }, + { + "name": '__EnumValue' + }, + { + "name": '__Directive' + }, + { + "name": '__DirectiveLocation' + } + ] + } + } + + result = graphql(StarWarsSchema, query) + assert not result.errors + assert contain_subset(result.data, expected) + + +def test_allows_querying_the_schema_for_query_type(): + query = ''' + query IntrospectionQueryTypeQuery { + __schema { + queryType { + name + } + } + } + ''' + + expected = { + '__schema': { + 'queryType': { + 'name': 'Query' + }, + } + } + result = graphql(StarWarsSchema, query) + assert not result.errors + assert contain_subset(result.data, expected) + + +def test_allows_querying_the_schema_for_a_specific_type(): + query = ''' + query IntrospectionDroidTypeQuery { + __type(name: "Droid") { + name + } + } + ''' + + expected = { + '__type': { + 'name': 'Droid' + } + } + result = graphql(StarWarsSchema, query) + assert not result.errors + assert contain_subset(result.data, expected) + + +def test_allows_querying_the_schema_for_an_object_kind(): + query = ''' + query IntrospectionDroidKindQuery { + __type(name: "Droid") { + name + kind + } + } + ''' + + expected = { + '__type': { + 'name': 'Droid', + 'kind': 'OBJECT' + } + } + result = graphql(StarWarsSchema, query) + assert not result.errors + assert contain_subset(result.data, expected) + + +def test_allows_querying_the_schema_for_an_interface_kind(): + query = ''' + query IntrospectionCharacterKindQuery { + __type(name: "Character") { + name + kind + } + } + ''' + expected = { + '__type': { + 'name': 'Character', + 'kind': 'INTERFACE' + } + } + result = graphql(StarWarsSchema, query) + assert not result.errors + assert contain_subset(result.data, expected) + + +def test_allows_querying_the_schema_for_object_fields(): + query = ''' + query IntrospectionDroidFieldsQuery { + __type(name: "Droid") { + name + fields { + name + type { + name + kind + } + } + } + } + ''' + + expected = { + '__type': { + 'name': 'Droid', + 'fields': [ + { + 'name': 'id', + 'type': { + 'name': None, + 'kind': 'NON_NULL' + } + }, + { + 'name': 'name', + 'type': { + 'name': 'String', + 'kind': 'SCALAR' + } + }, + { + 'name': 'friends', + 'type': { + 'name': None, + 'kind': 'LIST' + } + }, + { + 'name': 'appearsIn', + 'type': { + 'name': None, + 'kind': 'LIST' + } + }, + { + 'name': 'primaryFunction', + 'type': { + 'name': 'String', + 'kind': 'SCALAR' + } + } + ] + } + } + + result = graphql(StarWarsSchema, query) + assert not result.errors + assert contain_subset(result.data, expected) + + +def test_allows_querying_the_schema_for_nested_object_fields(): + query = ''' + query IntrospectionDroidNestedFieldsQuery { + __type(name: "Droid") { + name + fields { + name + type { + name + kind + ofType { + name + kind + } + } + } + } + } + ''' + + expected = { + '__type': { + 'name': 'Droid', + 'fields': [ + { + 'name': 'id', + 'type': { + 'name': None, + 'kind': 'NON_NULL', + 'ofType': { + 'name': 'String', + 'kind': 'SCALAR' + } + } + }, + { + 'name': 'name', + 'type': { + 'name': 'String', + 'kind': 'SCALAR', + 'ofType': None + } + }, + { + 'name': 'friends', + 'type': { + 'name': None, + 'kind': 'LIST', + 'ofType': { + 'name': 'Character', + 'kind': 'INTERFACE' + } + } + }, + { + 'name': 'appearsIn', + 'type': { + 'name': None, + 'kind': 'LIST', + 'ofType': { + 'name': 'Episode', + 'kind': 'ENUM' + } + } + }, + { + 'name': 'primaryFunction', + 'type': { + 'name': 'String', + 'kind': 'SCALAR', + 'ofType': None + } + } + ] + } + } + result = graphql(StarWarsSchema, query) + assert not result.errors + assert contain_subset(result.data, expected) + + +def test_allows_querying_the_schema_for_field_args(): + query = ''' + query IntrospectionQueryTypeQuery { + __schema { + queryType { + fields { + name + args { + name + description + type { + name + kind + ofType { + name + kind + } + } + defaultValue + } + } + } + } + } + ''' + + expected = { + '__schema': { + 'queryType': { + 'fields': [ + { + 'name': 'hero', + 'args': [ + { + 'defaultValue': None, + 'description': 'If omitted, returns the hero of the whole ' + + 'saga. If provided, returns the hero of ' + + 'that particular episode.', + 'name': 'episode', + 'type': { + 'kind': 'ENUM', + 'name': 'Episode', + 'ofType': None + } + } + ] + }, + { + 'name': 'human', + 'args': [ + { + 'name': 'id', + 'description': 'id of the human', + 'type': { + 'kind': 'NON_NULL', + 'name': None, + 'ofType': { + 'kind': 'SCALAR', + 'name': 'String' + } + }, + 'defaultValue': None + } + ] + }, + { + 'name': 'droid', + 'args': [ + { + 'name': 'id', + 'description': 'id of the droid', + 'type': { + 'kind': 'NON_NULL', + 'name': None, + 'ofType': { + 'kind': 'SCALAR', + 'name': 'String' + } + }, + 'defaultValue': None + } + ] + } + ] + } + } + } + + result = graphql(StarWarsSchema, query) + assert not result.errors + assert contain_subset(result.data, expected) + + +def test_allows_querying_the_schema_for_documentation(): + query = ''' + query IntrospectionDroidDescriptionQuery { + __type(name: "Droid") { + name + description + } + } + ''' + + expected = { + '__type': { + 'name': 'Droid', + 'description': 'A mechanical creature in the Star Wars universe.' + } + } + result = graphql(StarWarsSchema, query) + assert not result.errors + assert contain_subset(result.data, expected) diff --git a/tests/core_starwars/test_query.py b/tests/starwars/test_query.py similarity index 96% rename from tests/core_starwars/test_query.py rename to tests/starwars/test_query.py index 9b50c143..7490e633 100644 --- a/tests/core_starwars/test_query.py +++ b/tests/starwars/test_query.py @@ -1,5 +1,5 @@ -from graphql.core import graphql -from graphql.core.error import format_error +from graphql import graphql +from graphql.error import format_error from .starwars_schema import StarWarsSchema @@ -162,7 +162,7 @@ def test_fetch_some_id_query(): 'name': 'Luke Skywalker', } } - result = graphql(StarWarsSchema, query, None, params) + result = graphql(StarWarsSchema, query, variable_values=params) assert not result.errors assert result.data == expected @@ -183,7 +183,7 @@ def test_fetch_some_id_query2(): 'name': 'Han Solo', } } - result = graphql(StarWarsSchema, query, None, params) + result = graphql(StarWarsSchema, query, variable_values=params) assert not result.errors assert result.data == expected @@ -202,7 +202,7 @@ def test_invalid_id_query(): expected = { 'human': None } - result = graphql(StarWarsSchema, query, None, params) + result = graphql(StarWarsSchema, query, variable_values=params) assert not result.errors assert result.data == expected diff --git a/tests/core_starwars/test_validation.py b/tests/starwars/test_validation.py similarity index 93% rename from tests/core_starwars/test_validation.py rename to tests/starwars/test_validation.py index 68e16c74..bafa2328 100644 --- a/tests/core_starwars/test_validation.py +++ b/tests/starwars/test_validation.py @@ -1,6 +1,6 @@ -from graphql.core.language.parser import parse -from graphql.core.language.source import Source -from graphql.core.validation import validate +from graphql.language.parser import parse +from graphql.language.source import Source +from graphql.validation import validate from .starwars_schema import StarWarsSchema diff --git a/tests_py35/core_execution/test_asyncio_executor.py b/tests_py35/core_execution/test_asyncio_executor.py index 0f23e80e..cd085442 100644 --- a/tests_py35/core_execution/test_asyncio_executor.py +++ b/tests_py35/core_execution/test_asyncio_executor.py @@ -2,10 +2,11 @@ import asyncio import functools -from graphql.core.error import format_error -from graphql.core.execution import Executor -from graphql.core.execution.middlewares.asyncio import AsyncioExecutionMiddleware -from graphql.core.type import ( +from graphql.error import format_error +from graphql.execution import execute +from graphql.language.parser import parse +from graphql.execution.executors.asyncio import AsyncioExecutor +from graphql.type import ( GraphQLSchema, GraphQLObjectType, GraphQLField, @@ -13,17 +14,8 @@ ) -def run_until_complete(fun): - @functools.wraps(fun) - def wrapper(*args, **kwargs): - coro = fun(*args, **kwargs) - return asyncio.get_event_loop().run_until_complete(coro) - return wrapper - - -@run_until_complete -async def test_asyncio_py35_executor(): - doc = 'query Example { a, b, c }' +def test_asyncio_py35_executor(): + ast = parse('query Example { a, b, c }') async def resolver(context, *_): await asyncio.sleep(0.001) @@ -42,14 +34,13 @@ def resolver_3(context, *_): 'c': GraphQLField(GraphQLString, resolver=resolver_3) }) - executor = Executor([AsyncioExecutionMiddleware()]) - result = await executor.execute(GraphQLSchema(Type), doc) + result = execute(GraphQLSchema(Type), ast, executor=AsyncioExecutor()) assert not result.errors assert result.data == {'a': 'hey', 'b': 'hey2', 'c': 'hey3'} -@run_until_complete -async def test_asyncio_py35_executor_with_error(): - doc = 'query Example { a, b }' + +def test_asyncio_py35_executor_with_error(): + ast = parse('query Example { a, b }') async def resolver(context, *_): await asyncio.sleep(0.001) @@ -64,8 +55,7 @@ async def resolver_2(context, *_): 'b': GraphQLField(GraphQLString, resolver=resolver_2) }) - executor = Executor([AsyncioExecutionMiddleware()]) - result = await executor.execute(GraphQLSchema(Type), doc) + result = execute(GraphQLSchema(Type), ast, executor=AsyncioExecutor()) formatted_errors = list(map(format_error, result.errors)) assert formatted_errors == [{'locations': [{'line': 1, 'column': 20}], 'message': 'resolver_2 failed!'}] assert result.data == {'a': 'hey', 'b': None} diff --git a/tox.ini b/tox.ini index 06fa9a72..081df1ee 100644 --- a/tox.ini +++ b/tox.ini @@ -1,26 +1,27 @@ [tox] -envlist = flake8,import-order,py27,py33,py34,py35,pypy,docs +envlist = flake8,isort,py27,py33,py34,py35,pypy,docs [testenv] deps = pytest>=2.7.2 gevent==1.1rc1 six>=1.10.0 + pytest-mock commands = - py{27,33,34,py}: py.test tests {posargs} - py35: py.test tests tests_py35 {posargs} + py{27,33,34,py}: py.test graphql tests {posargs} + py35: py.test graphql tests tests_py35 {posargs} [testenv:flake8] deps = flake8 commands = flake8 -[testenv:import-order] +[testenv:isort] basepython=python3.5 deps = - import-order + isort==3.9.6 gevent==1.1rc1 -commands = import-order graphql +commands = isort -rc graphql [testenv:docs] changedir = docs