From b797aab04f372da33275a41c820e484c30064185 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Mon, 16 Mar 2020 21:00:05 -0500 Subject: [PATCH 01/17] feat: server-core compatible with graphql-core-v3 - Bump dependencies - Refactor code to use f-strings format (3.6+) - Rename public data structures BREAKING CHANGE: - Requires graphql-core-v3 - Drop support for Python 2 and below 3.6 - Remove executor check as graphql-core-v3 does not have SyncExecutor --- graphql_server/__init__.py | 243 ++++++++++++++----------------------- setup.py | 23 ++-- 2 files changed, 103 insertions(+), 163 deletions(-) diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index cb802ee..5efe0ee 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -9,53 +9,34 @@ import json -from collections import namedtuple +from collections import namedtuple, MutableMapping +from typing import Optional, List, Callable, Dict, Any, Union, Type -import six +from graphql import (GraphQLSchema, ExecutionResult, GraphQLError, parse, get_operation_ast, + validate_schema, validate, execute) +from graphql import format_error as format_error_default -from promise import promisify, is_thenable, Promise - -from graphql import get_default_backend -from graphql.error import format_error as default_format_error -from graphql.execution import ExecutionResult -from graphql.execution.executors.sync import SyncExecutor -from graphql.type import GraphQLSchema +from promise import promisify, Promise from .error import HttpQueryError -try: # pragma: no cover (Python >= 3.3) - from collections.abc import MutableMapping -except ImportError: # pragma: no cover (Python < 3.3) - # noinspection PyUnresolvedReferences,PyProtectedMember - from collections import MutableMapping - -# Necessary for static type checking -# noinspection PyUnreachableCode -if False: # pragma: no cover - # flake8: noqa - from typing import Any, Callable, Dict, List, Optional, Type, Union - from graphql import GraphQLBackend - __all__ = [ "run_http_query", "encode_execution_results", "load_json_body", "json_encode", - "json_encode_pretty", "HttpQueryError", - "RequestParams", - "ServerResults", + "GraphQLParams", + "GraphQLResponse", "ServerResponse", ] # The public data structures -RequestParams = namedtuple("RequestParams", "query variables operation_name") - -ServerResults = namedtuple("ServerResults", "results params") - +GraphQLParams = namedtuple("GraphQLParams", "query variables operation_name") +GraphQLResponse = namedtuple("GraphQLResponse", "results params") ServerResponse = namedtuple("ServerResponse", "body status_code") @@ -63,14 +44,14 @@ def run_http_query( - schema, # type: GraphQLSchema - request_method, # type: str - data, # type: Union[Dict, List[Dict]] - query_data=None, # type: Optional[Dict] - batch_enabled=False, # type: bool - catch=False, # type: bool - **execute_options # type: Any -): + schema: GraphQLSchema, + request_method: str, + data: Union[Dict, List[Dict]], + query_data: Optional[Dict] = None, + batch_enabled: bool = False, + catch: bool = False, + **execute_options: Dict[str, Any] +) -> GraphQLResponse: """Execute GraphQL coming from an HTTP query against a given schema. You need to pass the schema (that is supposed to be already validated), @@ -87,7 +68,7 @@ def run_http_query( and the list of parameters that have been used for execution as second item. """ if not isinstance(schema, GraphQLSchema): - raise TypeError("Expected a GraphQL schema, but received {!r}.".format(schema)) + raise TypeError(f"Expected a GraphQL schema, but received {schema!r}.") if request_method not in ("get", "post"): raise HttpQueryError( 405, @@ -95,9 +76,7 @@ def run_http_query( headers={"Allow": "GET, POST"}, ) if catch: - catch_exc = ( - HttpQueryError - ) # type: Union[Type[HttpQueryError], Type[_NoException]] + catch_exc: Union[Type[HttpQueryError], Type[_NoException]] = HttpQueryError else: catch_exc = _NoException is_batch = isinstance(data, list) @@ -108,7 +87,7 @@ def run_http_query( if not is_batch: if not isinstance(data, (dict, MutableMapping)): raise HttpQueryError( - 400, "GraphQL params should be a dict. Received {!r}.".format(data) + 400, f"GraphQL params should be a dict. Received {data!r}." ) data = [data] elif not batch_enabled: @@ -117,50 +96,35 @@ def run_http_query( if not data: raise HttpQueryError(400, "Received an empty list in the batch request.") - extra_data = {} # type: Dict[str, Any] + extra_data: Dict[str, Any] = {} # If is a batch request, we don't consume the data from the query if not is_batch: extra_data = query_data or {} - all_params = [get_graphql_params(entry, extra_data) for entry in data] + all_params: List[GraphQLParams] = [get_graphql_params(entry, extra_data) for entry in data] - if execute_options.get("return_promise"): - results = [ - get_response(schema, params, catch_exc, allow_only_query, **execute_options) - for params in all_params - ] - else: - executor = execute_options.get("executor") - response_executor = executor if executor else SyncExecutor() - - response_promises = [ - response_executor.execute( - get_response, - schema, - params, - catch_exc, - allow_only_query, - **execute_options - ) - for params in all_params - ] - response_executor.wait_until_finished() + results = [get_response(schema, params, catch_exc, allow_only_query, **execute_options) for params in all_params] + + return GraphQLResponse(results, all_params) - results = [ - result.get() if is_thenable(result) else result - for result in response_promises - ] - return ServerResults(results, all_params) +def json_encode(data: Union[Dict, List], pretty: bool = False) -> str: + """Serialize the given data(a dictionary or a list) using JSON. + + The given data (a dictionary or a list) will be serialized using JSON + and returned as a string that will be nicely formatted if you set pretty=True. + """ + if not pretty: + return json.dumps(data, separators=(",", ":")) + return json.dumps(data, indent=2, separators=(",", ": ")) def encode_execution_results( - execution_results, # type: List[Optional[ExecutionResult]] - format_error=None, # type: Callable[[Exception], Dict] - is_batch=False, # type: bool - encode=None, # type: Callable[[Dict], Any] -): - # type: (...) -> ServerResponse + execution_results: List[Optional[ExecutionResult]], + format_error: Callable[[Exception], Dict], + is_batch: bool = False, + encode: Callable[[Dict], Any] = json_encode, +) -> ServerResponse: """Serialize the ExecutionResults. This function takes the ExecutionResults that are returned by run_http_query() @@ -174,7 +138,7 @@ def encode_execution_results( a status code of 200 or 400 in case any result was invalid as the second item. """ results = [ - format_execution_result(execution_result, format_error or default_format_error) + format_execution_result(execution_result, format_error) for execution_result in execution_results ] result, status_codes = zip(*results) @@ -183,7 +147,7 @@ def encode_execution_results( if not is_batch: result = result[0] - return ServerResponse((encode or json_encode)(result), status_code) + return ServerResponse(encode(result), status_code) def load_json_body(data): @@ -199,24 +163,6 @@ def load_json_body(data): raise HttpQueryError(400, "POST body sent invalid JSON.") -def json_encode(data, pretty=False): - # type: (Union[Dict,List],Optional[bool]) -> str - """Serialize the given data(a dictionary or a list) using JSON. - - The given data (a dictionary or a list) will be serialized using JSON - and returned as a string that will be nicely formatted if you set pretty=True. - """ - if pretty: - return json_encode_pretty(data) - return json.dumps(data, separators=(",", ":")) - - -def json_encode_pretty(data): - # type: (Union[Dict,List]) -> str - """Serialize the given data using JSON with nice formatting.""" - return json.dumps(data, indent=2, separators=(",", ": ")) - - # Some more private helpers FormattedResult = namedtuple("FormattedResult", "result status_code") @@ -226,8 +172,7 @@ class _NoException(Exception): """Private exception used when we don't want to catch any real exception.""" -def get_graphql_params(data, query_data): - # type: (Dict, Dict) -> RequestParams +def get_graphql_params(data: Dict, query_data: Dict) -> GraphQLParams: """Fetch GraphQL query, variables and operation name parameters from given data. You need to pass both the data from the HTTP request body and the HTTP query string. @@ -240,18 +185,17 @@ def get_graphql_params(data, query_data): # document_id = data.get('documentId') operation_name = data.get("operationName") or query_data.get("operationName") - return RequestParams(query, load_json_variables(variables), operation_name) + return GraphQLParams(query, load_json_variables(variables), operation_name) -def load_json_variables(variables): - # type: (Optional[Union[str, Dict]]) -> Optional[Dict] +def load_json_variables(variables: Optional[Union[str, Dict]]) -> Optional[Dict]: """Return the given GraphQL variables as a dictionary. The function returns the given GraphQL variables, making sure they are deserialized from JSON to a dictionary first if necessary. In case of invalid JSON input, an HttpQueryError will be raised. """ - if variables and isinstance(variables, six.string_types): + if variables and isinstance(variables, str): try: return json.loads(variables) except Exception: @@ -260,13 +204,11 @@ def load_json_variables(variables): def execute_graphql_request( - schema, # type: GraphQLSchema - params, # type: RequestParams - allow_only_query=False, # type: bool - backend=None, # type: GraphQLBackend - **kwargs # type: Any -): - # type: (...) -> ExecutionResult + schema: GraphQLSchema, + params: GraphQLParams, + allow_only_query: bool = False, + **kwargs +) -> ExecutionResult: """Execute a GraphQL request and return an ExecutionResult. You need to pass the GraphQL schema and the GraphQLParams that you can get @@ -279,32 +221,37 @@ def execute_graphql_request( if not params.query: raise HttpQueryError(400, "Must provide query string.") + # Validate the schema and return a list of errors if it does not satisfy the Type System. + schema_validation_errors = validate_schema(schema) + if schema_validation_errors: + return ExecutionResult(data=None, errors=schema_validation_errors) + + # Parse the query and return ExecutionResult with errors found. + # Any Exception is parsed as GraphQLError. try: - if not backend: - backend = get_default_backend() - document = backend.document_from_string(schema, params.query) + document = parse(params.query) + except GraphQLError as e: + return ExecutionResult(data=None, errors=[e]) except Exception as e: - return ExecutionResult(errors=[e], invalid=True) + e = GraphQLError(str(e), original_error=e) + return ExecutionResult(data=None, errors=[e]) if allow_only_query: - operation_type = document.get_operation_type(params.operation_name) - if operation_type and operation_type != "query": - raise HttpQueryError( - 405, - "Can only perform a {} operation from a POST request.".format( - operation_type - ), - headers={"Allow": "POST"}, - ) + operation_ast = get_operation_ast(document, params.operation_name) + if operation_ast: + operation = operation_ast.operation.value + if operation != 'query': + raise HttpQueryError( + 405, + f"Can only perform a {operation} operation from a POST request.", + headers={"Allow": "POST"}, + ) - try: - return document.execute( - operation_name=params.operation_name, - variable_values=params.variables, - **kwargs - ) - except Exception as e: - return ExecutionResult(errors=[e], invalid=True) + validation_errors = validate(schema, document) + if validation_errors: + return ExecutionResult(data=None, errors=validation_errors) + + return execute(schema, document, variable_values=params.variables, operation_name=params.operation_name, **kwargs) @promisify @@ -313,28 +260,25 @@ def execute_graphql_request_as_promise(*args, **kwargs): def get_response( - schema, # type: GraphQLSchema - params, # type: RequestParams - catch_exc, # type: Type[BaseException] - allow_only_query=False, # type: bool - **kwargs # type: Any -): - # type: (...) -> Optional[Union[ExecutionResult, Promise[ExecutionResult]]] + schema: GraphQLSchema, + params: GraphQLParams, + catch_exc: Type[BaseException], + allow_only_query: bool = False, + **kwargs +) -> Optional[Union[ExecutionResult, Promise[ExecutionResult]]]: """Get an individual execution result as response, with option to catch errors. This does the same as execute_graphql_request() except that you can catch errors that belong to an exception class that you need to pass as a parameter. """ # Note: PyCharm will display a error due to the triple dot being used on Callable. - execute = ( - execute_graphql_request - ) # type: Callable[..., Union[Promise[ExecutionResult], ExecutionResult]] + execute_request: Callable[..., Union[Promise[ExecutionResult], ExecutionResult]] = execute_graphql_request if kwargs.get("return_promise", False): - execute = execute_graphql_request_as_promise + execute_request = execute_graphql_request_as_promise # noinspection PyBroadException try: - execution_result = execute(schema, params, allow_only_query, **kwargs) + execution_result = execute_request(schema, params, allow_only_query, **kwargs) except catch_exc: return None @@ -342,21 +286,22 @@ def get_response( def format_execution_result( - execution_result, # type: Optional[ExecutionResult] - format_error, # type: Optional[Callable[[Exception], Dict]] -): - # type: (...) -> FormattedResult + execution_result: Optional[ExecutionResult], + format_error: Optional[Callable[[Exception], Dict]] = format_error_default +) -> GraphQLResponse: """Format an execution result into a GraphQLResponse. This converts the given execution result into a FormattedResult that contains the ExecutionResult converted to a dictionary and an appropriate status code. """ status_code = 200 + response: Optional[Dict[str, Any]] = None - response = None if execution_result: - if execution_result.invalid: + if execution_result.errors: + response = {"errors": [format_error(e) for e in execution_result.errors]} status_code = 400 - response = execution_result.to_dict(format_error=format_error) + else: + response = {'data': execution_result.data} return FormattedResult(response, status_code) diff --git a/setup.py b/setup.py index a6416c0..fda8577 100644 --- a/setup.py +++ b/setup.py @@ -1,28 +1,28 @@ from setuptools import setup, find_packages install_requires = [ - "graphql-core>=2.3,<3", + "graphql-core>=3,<4", "promise>=2.3,<3", ] tests_requires = [ - "pytest==4.6.9", - "pytest-cov==2.8.1" + "pytest>=5.3,<5.4", + "pytest-cov>=2.8,<3", ] dev_requires = [ - 'flake8==3.7.9', - 'isort<4.0.0', - 'black==19.10b0', - 'mypy==0.761', - 'check-manifest>=0.40,<1', + "flake8>=3.7,<4", + "isort>=4,<5", + "black==19.10b0", + "mypy>=0.761,<0.770", + "check-manifest>=0.40,<1", ] + tests_requires setup( name="graphql-server-core", version="2.0.0", description="GraphQL Server tools for powering your server", - long_description=open("README.md").read(), + long_description=open("README.md", encoding="utf-8").read(), long_description_content_type="text/markdown", url="https://github.com/graphql-python/graphql-server-core", download_url="https://github.com/graphql-python/graphql-server-core/releases", @@ -33,14 +33,9 @@ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: Implementation :: PyPy", "License :: OSI Approved :: MIT License", ], keywords="api graphql protocol rest", From 8c950f014722d9acc4f886f77c96fc7ff1c478c2 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Mon, 16 Mar 2020 21:12:58 -0500 Subject: [PATCH 02/17] chore: drop unsupported py versions on tox and travis --- .travis.yml | 5 +---- tox.ini | 12 ++++++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7789878..ee93dd9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,14 @@ language: python sudo: false python: - - 2.7 - - 3.5 - 3.6 - 3.7 - 3.8 - 3.9-dev - - pypy - pypy3 matrix: include: - - python: 3.6 + - python: 3.7 env: TOXENV=flake8,black,import-order,mypy,manifest cache: pip install: pip install tox-travis codecov diff --git a/tox.ini b/tox.ini index 77a2bb6..16b4fd0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = black,flake8,import-order,mypy,manifest, - py{27,35,36,37,38,39-dev,py,py3} + py{36,37,38,39-dev,py3} ; requires = tox-conda [testenv] @@ -17,31 +17,31 @@ commands = pytest --cov-report=term-missing --cov=graphql_server tests {posargs} [testenv:black] -basepython=python3.6 +basepython=python3.7 deps = -e.[dev] commands = black --check graphql_server tests [testenv:flake8] -basepython=python3.6 +basepython=python3.7 deps = -e.[dev] commands = flake8 setup.py graphql_server tests [testenv:import-order] -basepython=python3.6 +basepython=python3.7 deps = -e.[dev] commands = isort -rc graphql_server/ tests/ [testenv:mypy] -basepython=python3.6 +basepython=python3.7 deps = -e.[dev] commands = mypy graphql_server tests --ignore-missing-imports [testenv:manifest] -basepython = python3.6 +basepython = python3.7 deps = -e.[dev] commands = check-manifest -v From 6814e189e8aac6f198579760967c2e0b5354be05 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Mon, 16 Mar 2020 23:10:09 -0500 Subject: [PATCH 03/17] tests: apply minor fixes to older tests --- graphql_server/__init__.py | 6 +- tests/conftest.py | 4 - tests/schema.py | 36 ++--- tests/test_asyncio.py | 88 ----------- tests/test_error.py | 44 +++--- tests/test_helpers.py | 136 +--------------- tests/test_query.py | 312 ++++++++++++++----------------------- tests/utils.py | 13 +- 8 files changed, 178 insertions(+), 461 deletions(-) delete mode 100644 tests/conftest.py delete mode 100644 tests/test_asyncio.py diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index 5efe0ee..6a018bc 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -9,7 +9,8 @@ import json -from collections import namedtuple, MutableMapping +from collections import namedtuple +from collections.abc import MutableMapping from typing import Optional, List, Callable, Dict, Any, Union, Type from graphql import (GraphQLSchema, ExecutionResult, GraphQLError, parse, get_operation_ast, @@ -30,6 +31,7 @@ "GraphQLParams", "GraphQLResponse", "ServerResponse", + "format_execution_result" ] @@ -121,7 +123,7 @@ def json_encode(data: Union[Dict, List], pretty: bool = False) -> str: def encode_execution_results( execution_results: List[Optional[ExecutionResult]], - format_error: Callable[[Exception], Dict], + format_error: Callable[[Exception], Dict] = format_error_default, is_batch: bool = False, encode: Callable[[Dict], Any] = json_encode, ) -> ServerResponse: diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index ae78c3d..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,4 +0,0 @@ -import sys - -if sys.version_info[:2] < (3, 4): - collect_ignore_glob = ["*_asyncio.py"] diff --git a/tests/schema.py b/tests/schema.py index c60b0ed..d8d7827 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -1,41 +1,35 @@ -from graphql.type.definition import ( +from graphql import ( GraphQLArgument, GraphQLField, GraphQLNonNull, GraphQLObjectType, + GraphQLString, + GraphQLSchema, ) -from graphql.type.scalars import GraphQLString -from graphql.type.schema import GraphQLSchema -def resolve_error(*_args): - raise ValueError("Throws!") +def resolve_thrower(*_args): + raise Exception("Throws!") def resolve_request(_obj, info): - return info.context.get("q") + return info.context.get('q') def resolve_context(_obj, info): return str(info.context) -def resolve_test(_obj, _info, who="World"): - return "Hello {}".format(who) - - -NonNullString = GraphQLNonNull(GraphQLString) - QueryRootType = GraphQLObjectType( name="QueryRoot", fields={ - "error": GraphQLField(NonNullString, resolver=resolve_error), - "request": GraphQLField(NonNullString, resolver=resolve_request), - "context": GraphQLField(NonNullString, resolver=resolve_context), + "thrower": GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_thrower), + "request": GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_request), + "context": GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_context), "test": GraphQLField( - GraphQLString, - {"who": GraphQLArgument(GraphQLString)}, - resolver=resolve_test, + type_=GraphQLString, + args={"who": GraphQLArgument(GraphQLString)}, + resolve=lambda obj, info, who="World": "Hello %s" % who, ), }, ) @@ -43,10 +37,8 @@ def resolve_test(_obj, _info, who="World"): MutationRootType = GraphQLObjectType( name="MutationRoot", fields={ - "writeTest": GraphQLField( - type=QueryRootType, resolver=lambda *_args: QueryRootType - ) + "writeTest": GraphQLField(type_=QueryRootType, resolve=lambda *_: QueryRootType) }, ) -schema = GraphQLSchema(QueryRootType, MutationRootType) +schema = GraphQLSchema(QueryRootType, MutationRootType) \ No newline at end of file diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py deleted file mode 100644 index db8fc02..0000000 --- a/tests/test_asyncio.py +++ /dev/null @@ -1,88 +0,0 @@ -from graphql.execution.executors.asyncio import AsyncioExecutor -from graphql.type.definition import GraphQLField, GraphQLNonNull, GraphQLObjectType -from graphql.type.scalars import GraphQLString -from graphql.type.schema import GraphQLSchema -from promise import Promise - -import asyncio -from graphql_server import RequestParams, run_http_query - -from .utils import as_dicts - - -def resolve_error_sync(_obj, _info): - raise ValueError("error sync") - - -async def resolve_error_async(_obj, _info): - await asyncio.sleep(0.001) - raise ValueError("error async") - - -def resolve_field_sync(_obj, _info): - return "sync" - - -async def resolve_field_async(_obj, info): - await asyncio.sleep(0.001) - return "async" - - -NonNullString = GraphQLNonNull(GraphQLString) - -QueryRootType = GraphQLObjectType( - name="QueryRoot", - fields={ - "errorSync": GraphQLField(NonNullString, resolver=resolve_error_sync), - "errorAsync": GraphQLField(NonNullString, resolver=resolve_error_async), - "fieldSync": GraphQLField(NonNullString, resolver=resolve_field_sync), - "fieldAsync": GraphQLField(NonNullString, resolver=resolve_field_async), - }, -) - -schema = GraphQLSchema(QueryRootType) - - -def test_get_responses_using_asyncio_executor(): - class TestExecutor(AsyncioExecutor): - called = False - waited = False - cleaned = False - - def wait_until_finished(self): - TestExecutor.waited = True - super().wait_until_finished() - - def clean(self): - TestExecutor.cleaned = True - super().clean() - - def execute(self, fn, *args, **kwargs): - TestExecutor.called = True - return super().execute(fn, *args, **kwargs) - - query = "{fieldSync fieldAsync}" - - loop = asyncio.get_event_loop() - - async def get_results(): - result_promises, params = run_http_query( - schema, - "get", - {}, - dict(query=query), - executor=TestExecutor(loop=loop), - return_promise=True, - ) - results = await Promise.all(result_promises) - return results, params - - results, params = loop.run_until_complete(get_results()) - - expected_results = [{"data": {"fieldSync": "sync", "fieldAsync": "async"}}] - - assert as_dicts(results) == expected_results - assert params == [RequestParams(query=query, variables=None, operation_name=None)] - assert TestExecutor.called - assert not TestExecutor.waited - assert TestExecutor.cleaned diff --git a/tests/test_error.py b/tests/test_error.py index a0f7017..4dfdc93 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -1,28 +1,34 @@ from graphql_server import HttpQueryError -def test_create_http_query_error(): - - error = HttpQueryError(420, "Some message", headers={"SomeHeader": "SomeValue"}) - assert error.status_code == 420 - assert error.message == "Some message" - assert error.headers == {"SomeHeader": "SomeValue"} +def test_can_create_http_query_error(): + error = HttpQueryError(400, "Bad error") + assert error.status_code == 400 + assert error.message == "Bad error" + assert not error.is_graphql_error + assert error.headers is None def test_compare_http_query_errors(): - - error = HttpQueryError(400, "Message", headers={"Header": "Value"}) - assert error == HttpQueryError(400, "Message", headers={"Header": "Value"}) - assert error != HttpQueryError(420, "Message", headers={"Header": "Value"}) - assert error != HttpQueryError(400, "Other Message", headers={"Header": "Value"}) - assert error != HttpQueryError(400, "Message", headers={"Header": "OtherValue"}) + error = HttpQueryError(400, "Bad error") + assert error == error + same_error = HttpQueryError(400, "Bad error") + assert error == same_error + different_error = HttpQueryError(400, "Not really bad error") + assert error != different_error + different_error = HttpQueryError(405, "Bad error") + assert error != different_error + different_error = HttpQueryError(400, "Bad error", headers={"Allow": "ALL"}) + assert error != different_error def test_hash_http_query_errors(): - - error = HttpQueryError(400, "Foo", headers={"Bar": "Baz"}) - - assert hash(error) == hash(HttpQueryError(400, "Foo", headers={"Bar": "Baz"})) - assert hash(error) != hash(HttpQueryError(420, "Foo", headers={"Bar": "Baz"})) - assert hash(error) != hash(HttpQueryError(400, "Boo", headers={"Bar": "Baz"})) - assert hash(error) != hash(HttpQueryError(400, "Foo", headers={"Bar": "Faz"})) + errors = { + HttpQueryError(400, "Bad error 1"), + HttpQueryError(400, "Bad error 2"), + HttpQueryError(403, "Bad error 1"), + } + assert HttpQueryError(400, "Bad error 1") in errors + assert HttpQueryError(400, "Bad error 2") in errors + assert HttpQueryError(403, "Bad error 1") in errors + assert HttpQueryError(403, "Bad error 2") not in errors diff --git a/tests/test_helpers.py b/tests/test_helpers.py index fc4b73e..5710b03 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,8 +1,8 @@ import json +from graphql import Source from graphql.error import GraphQLError from graphql.execution import ExecutionResult -from graphql.language.location import SourceLocation from pytest import raises from graphql_server import ( @@ -10,7 +10,6 @@ ServerResponse, encode_execution_results, json_encode, - json_encode_pretty, load_json_body, ) @@ -20,11 +19,6 @@ def test_json_encode(): assert result == '{"query":"{test}"}' -def test_json_encode_pretty(): - result = json_encode_pretty({"query": "{test}"}) - assert result == '{\n "query": "{test}"\n}' - - def test_json_encode_with_pretty_argument(): result = json_encode({"query": "{test}"}, pretty=False) assert result == '{"query":"{test}"}' @@ -88,7 +82,7 @@ def test_encode_execution_results_with_error(): None, [ GraphQLError( - "Some error", locations=[SourceLocation(1, 2)], path=["somePath"] + "Some error", source=Source(body="Some error"), positions=[1], path=["somePath"] ) ], ), @@ -100,7 +94,6 @@ def test_encode_execution_results_with_error(): assert isinstance(output.body, str) assert isinstance(output.status_code, int) assert json.loads(output.body) == { - "data": None, "errors": [ { "message": "Some error", @@ -109,26 +102,6 @@ def test_encode_execution_results_with_error(): } ], } - assert output.status_code == 200 - - -def test_encode_execution_results_with_invalid(): - execution_results = [ - ExecutionResult( - None, - [GraphQLError("SyntaxError", locations=[SourceLocation(1, 2)])], - invalid=True, - ), - ExecutionResult({"result": 42}, None), - ] - - output = encode_execution_results(execution_results) - assert isinstance(output, ServerResponse) - assert isinstance(output.body, str) - assert isinstance(output.status_code, int) - assert json.loads(output.body) == { - "errors": [{"message": "SyntaxError", "locations": [{"line": 1, "column": 2}]}] - } assert output.status_code == 400 @@ -149,7 +122,7 @@ def test_encode_execution_results_with_format_error(): None, [ GraphQLError( - "Some msg", locations=[SourceLocation(1, 2)], path=["some", "path"] + "Some msg", source=Source("Some msg"), positions=[1], path=["some", "path"] ) ], ) @@ -157,7 +130,7 @@ def test_encode_execution_results_with_format_error(): def format_error(error): return { - "msg": str(error), + "msg": error.message, "loc": "{}:{}".format(error.locations[0].line, error.locations[0].column), "pth": "/".join(error.path), } @@ -167,10 +140,9 @@ def format_error(error): assert isinstance(output.body, str) assert isinstance(output.status_code, int) assert json.loads(output.body) == { - "data": None, "errors": [{"msg": "Some msg", "loc": "1:2", "pth": "some/path"}], } - assert output.status_code == 200 + assert output.status_code == 400 def test_encode_execution_results_with_batch(): @@ -211,88 +183,6 @@ def test_encode_execution_results_with_batch_and_empty_result(): assert output.status_code == 200 -def test_encode_execution_results_with_batch_and_error(): - execution_results = [ - ExecutionResult({"result": 1}, None), - ExecutionResult( - None, - [ - GraphQLError( - "No data here", locations=[SourceLocation(1, 2)], path=["somePath"] - ) - ], - ), - ExecutionResult({"result": 3}, None), - ] - - output = encode_execution_results(execution_results, is_batch=True) - assert isinstance(output, ServerResponse) - assert isinstance(output.body, str) - assert isinstance(output.status_code, int) - assert json.loads(output.body) == [ - {"data": {"result": 1}}, - { - "data": None, - "errors": [ - { - "message": "No data here", - "locations": [{"line": 1, "column": 2}], - "path": ["somePath"], - } - ], - }, - {"data": {"result": 3}}, - ] - assert output.status_code == 200 - - -def test_encode_execution_results_with_batch_and_invalid(): - execution_results = [ - ExecutionResult({"result": 1}, None), - ExecutionResult( - None, - [ - GraphQLError( - "No data here", locations=[SourceLocation(1, 2)], path=["somePath"] - ) - ], - ), - ExecutionResult({"result": 3}, None), - ExecutionResult( - None, - [GraphQLError("SyntaxError", locations=[SourceLocation(1, 2)])], - invalid=True, - ), - ExecutionResult({"result": 5}, None), - ] - - output = encode_execution_results(execution_results, is_batch=True) - assert isinstance(output, ServerResponse) - assert isinstance(output.body, str) - assert isinstance(output.status_code, int) - assert json.loads(output.body) == [ - {"data": {"result": 1}}, - { - "data": None, - "errors": [ - { - "message": "No data here", - "locations": [{"line": 1, "column": 2}], - "path": ["somePath"], - } - ], - }, - {"data": {"result": 3}}, - { - "errors": [ - {"message": "SyntaxError", "locations": [{"line": 1, "column": 2}]} - ] - }, - {"data": {"result": 5}}, - ] - assert output.status_code == 400 - - def test_encode_execution_results_with_encode(): execution_results = [ExecutionResult({"result": None}, None)] @@ -305,19 +195,3 @@ def encode(result): assert isinstance(output.status_code, int) assert output.body == "{'data': {'result': None}}" assert output.status_code == 200 - - -def test_encode_execution_results_with_pretty(): - execution_results = [ExecutionResult({"test": "Hello World"}, None)] - - output = encode_execution_results(execution_results, encode=json_encode_pretty) - body = output.body - assert body == "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" - - -def test_encode_execution_results_not_pretty_by_default(): - execution_results = [ExecutionResult({"test": "Hello World"}, None)] - # execution_results = [ExecutionResult({"result": None}, None)] - - output = encode_execution_results(execution_results) - assert output.body == '{"data":{"test":"Hello World"}}' diff --git a/tests/test_query.py b/tests/test_query.py index e5bbb79..e0c26e8 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,19 +1,19 @@ import json +from typing import List from graphql.error import GraphQLError, GraphQLSyntaxError from graphql.execution import ExecutionResult -from promise import Promise from pytest import raises from graphql_server import ( HttpQueryError, - RequestParams, - ServerResults, - encode_execution_results, + GraphQLParams, + GraphQLResponse, json_encode, - json_encode_pretty, load_json_body, run_http_query, + format_execution_result, + encode_execution_results ) from .schema import schema @@ -21,23 +21,23 @@ def test_request_params(): - assert issubclass(RequestParams, tuple) + assert issubclass(GraphQLParams, tuple) # noinspection PyUnresolvedReferences - assert RequestParams._fields == ("query", "variables", "operation_name") + assert GraphQLParams._fields == ("query", "variables", "operation_name") def test_server_results(): - assert issubclass(ServerResults, tuple) + assert issubclass(GraphQLResponse, tuple) # noinspection PyUnresolvedReferences - assert ServerResults._fields == ("results", "params") + assert GraphQLResponse._fields == ("results", "params") def test_allows_get_with_query_param(): query = "{test}" results, params = run_http_query(schema, "get", {}, dict(query=query)) - assert as_dicts(results) == [{"data": {"test": "Hello World"}}] - assert params == [RequestParams(query=query, variables=None, operation_name=None)] + assert as_dicts(results) == [{"data": {"test": "Hello World"}, "errors": None}] + assert params == [GraphQLParams(query=query, variables=None, operation_name=None)] def test_allows_get_with_variable_values(): @@ -51,7 +51,7 @@ def test_allows_get_with_variable_values(): ), ) - assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}, "errors": None}] def test_allows_get_with_operation_name(): @@ -72,9 +72,7 @@ def test_allows_get_with_operation_name(): ), ) - assert as_dicts(results) == [ - {"data": {"test": "Hello World", "shared": "Hello Everyone"}} - ] + assert as_dicts(results) == [{"data": {"test": "Hello World", "shared": "Hello Everyone"}, "errors": None}] def test_reports_validation_errors(): @@ -84,14 +82,17 @@ def test_reports_validation_errors(): assert as_dicts(results) == [ { + "data": None, "errors": [ { - "message": 'Cannot query field "unknownOne" on type "QueryRoot".', + "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], + "path": None }, { - "message": 'Cannot query field "unknownTwo" on type "QueryRoot".', + "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 21}], + "path": None }, ] } @@ -132,12 +133,15 @@ def test_errors_when_missing_operation_name(): assert as_dicts(results) == [ { + "data": None, "errors": [ { + "locations": None, "message": ( "Must provide operation name" " if query contains multiple operations." - ) + ), + "path": None } ] } @@ -217,7 +221,7 @@ def test_allows_mutation_to_exist_within_a_get(): ), ) - assert as_dicts(results) == [{"data": {"test": "Hello World"}}] + assert as_dicts(results) == [{"data": {"test": "Hello World"}, "errors": None}] def test_allows_sending_a_mutation_via_post(): @@ -228,7 +232,7 @@ def test_allows_sending_a_mutation_via_post(): query_data=dict(query="mutation TestMutation { writeTest { test } }"), ) - assert as_dicts(results) == [{"data": {"writeTest": {"test": "Hello World"}}}] + assert results == [({"writeTest": {"test": "Hello World"}}, None)] def test_allows_post_with_url_encoding(): @@ -236,7 +240,7 @@ def test_allows_post_with_url_encoding(): schema, "post", {}, query_data=dict(query="{test}") ) - assert as_dicts(results) == [{"data": {"test": "Hello World"}}] + assert results == [({"test": "Hello World"}, None)] def test_supports_post_json_query_with_string_variables(): @@ -250,7 +254,20 @@ def test_supports_post_json_query_with_string_variables(): ), ) - assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + assert results == [({"test": "Hello Dolly"}, None)] + + +def test_supports_post_json_query_with_json_variables(): + result = load_json_body( + """ + { + "query": "query helloWho($who: String){ test(who: $who) }", + "variables": {"who": "Dolly"} + } + """ + ) + + assert result["variables"] == {"who": "Dolly"} def test_supports_post_url_encoded_query_with_string_variables(): @@ -264,7 +281,7 @@ def test_supports_post_url_encoded_query_with_string_variables(): ), ) - assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + assert results == [({"test": "Hello Dolly"}, None)] def test_supports_post_json_query_with_get_variable_values(): @@ -275,7 +292,7 @@ def test_supports_post_json_query_with_get_variable_values(): query_data=dict(variables={"who": "Dolly"}), ) - assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + assert results == [({"test": "Hello Dolly"}, None)] def test_post_url_encoded_query_with_get_variable_values(): @@ -286,7 +303,7 @@ def test_post_url_encoded_query_with_get_variable_values(): query_data=dict(variables='{"who": "Dolly"}'), ) - assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + assert results == [({"test": "Hello Dolly"}, None)] def test_supports_post_raw_text_query_with_get_variable_values(): @@ -297,7 +314,7 @@ def test_supports_post_raw_text_query_with_get_variable_values(): query_data=dict(variables='{"who": "Dolly"}'), ) - assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + assert results == [({"test": "Hello Dolly"}, None)] def test_allows_post_with_operation_name(): @@ -317,9 +334,7 @@ def test_allows_post_with_operation_name(): ), ) - assert as_dicts(results) == [ - {"data": {"test": "Hello World", "shared": "Hello Everyone"}} - ] + assert results == [({"test": "Hello World", "shared": "Hello Everyone"}, None)] def test_allows_post_with_get_operation_name(): @@ -339,55 +354,46 @@ def test_allows_post_with_get_operation_name(): query_data=dict(operationName="helloWorld"), ) - assert as_dicts(results) == [ - {"data": {"test": "Hello World", "shared": "Hello Everyone"}} - ] + assert results == [({"test": "Hello World", "shared": "Hello Everyone"}, None)] def test_supports_pretty_printing_data(): - results, params = run_http_query(schema, "get", dict(query="{test}")) - body = encode_execution_results(results, encode=json_encode_pretty).body + results, params = run_http_query(schema, "get", data=dict(query="{test}")) + result = {"data": results[0].data} - assert body == "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" + assert json_encode(result, pretty=True) == ( + "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" + ) def test_not_pretty_data_by_default(): - results, params = run_http_query(schema, "get", dict(query="{test}")) - body = encode_execution_results(results).body + results, params = run_http_query(schema, "get", data=dict(query="{test}")) + result = {"data": results[0].data} - assert body == '{"data":{"test":"Hello World"}}' + assert json_encode(result) == '{"data":{"test":"Hello World"}}' def test_handles_field_errors_caught_by_graphql(): - results, params = run_http_query(schema, "get", dict(query="{error}")) + results, params = run_http_query(schema, "get", data=dict(query="{thrower}")) - assert as_dicts(results) == [ - { - "data": None, - "errors": [ - { - "message": "Throws!", - "locations": [{"line": 1, "column": 2}], - "path": ["error"], - } - ], - } + assert results == [ + (None, [{"message": "Throws!", "locations": [(1, 2)], "path": ["thrower"]}]) ] def test_handles_syntax_errors_caught_by_graphql(): - results, params = run_http_query(schema, "get", dict(query="syntaxerror")) + results, params = run_http_query(schema, "get", data=dict(query="syntaxerror")) - assert as_dicts(results) == [ - { - "errors": [ + assert results == [ + ( + None, + [ { - "locations": [{"line": 1, "column": 1}], - "message": "Syntax Error GraphQL (1:1)" - ' Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n', + "locations": [(1, 1)], + "message": "Syntax Error: Unexpected Name 'syntaxerror'", } - ] - } + ], + ) ] @@ -401,9 +407,7 @@ def test_handles_errors_caused_by_a_lack_of_query(): def test_handles_errors_caused_by_invalid_query_type(): results, params = run_http_query(schema, "get", dict(query=42)) - assert as_dicts(results) == [ - {"errors": [{"message": "The query must be a string"}]} - ] + assert results == [(None, [{'message': 'Must provide Source. Received: 42'}])] def test_handles_batch_correctly_if_is_disabled(): @@ -447,10 +451,10 @@ def test_handles_poorly_formed_variables(): def test_handles_bad_schema(): with raises(TypeError) as exc_info: # noinspection PyTypeChecker - run_http_query("not a schema", "get", {"query": "{error}"}) # type: ignore + run_http_query("not a schema", "get", {}) # type: ignore - msg = str(exc_info.value) - assert msg == "Expected a GraphQL schema, but received 'not a schema'." + assert str(exc_info.value) == ( + "Expected a GraphQL schema, but received 'not a schema'.") def test_handles_unsupported_http_methods(): @@ -464,12 +468,54 @@ def test_handles_unsupported_http_methods(): ) +def test_format_execution_result(): + result = format_execution_result(None) + assert result == GraphQLResponse(None, 200) + data = {"answer": 42} + result = format_execution_result(ExecutionResult(data, None)) + assert result == GraphQLResponse({"data": data}, 200) + errors = [GraphQLError("bad")] + result = format_execution_result(ExecutionResult(None, errors)) + assert result == GraphQLResponse({"errors": errors}, 400) + + +def test_encode_execution_results(): + data = {"answer": 42} + errors = [GraphQLError("bad")] + results = [ExecutionResult(data, None), ExecutionResult(None, errors)] + result = encode_execution_results(results) + assert result == ('{"data":{"answer":42}}', 400) + + +def test_encode_execution_results_batch(): + data = {"answer": 42} + errors = [GraphQLError("bad")] + results = [ExecutionResult(data, None), ExecutionResult(None, errors)] + result = encode_execution_results(results, is_batch=True) + assert result == ( + '[{"data":{"answer":42}},' + '{"errors":[{"message":"bad","locations":null,"path":null}]}]', + 400, + ) + + +def test_encode_execution_results_not_encoded(): + data = {"answer": 42} + results = [ExecutionResult(data, None)] + result = encode_execution_results(results, encode=lambda r: r) + assert result == ({"data": data}, 200) + + def test_passes_request_into_request_context(): results, params = run_http_query( - schema, "get", {}, dict(query="{request}"), context_value={"q": "testing"} + schema, + "get", + {}, + query_data=dict(query="{request}"), + context_value={"q": "testing"}, ) - assert as_dicts(results) == [{"data": {"request": "testing"}}] + assert results == [({"request": "testing"}, None)] def test_supports_pretty_printing_context(): @@ -478,24 +524,24 @@ def __str__(self): return "CUSTOM CONTEXT" results, params = run_http_query( - schema, "get", {}, dict(query="{context}"), context_value=Context() + schema, "get", {}, query_data=dict(query="{context}"), context_value=Context() ) - assert as_dicts(results) == [{"data": {"context": "CUSTOM CONTEXT"}}] + assert results == [({"context": "CUSTOM CONTEXT"}, None)] def test_post_multipart_data(): query = "mutation TestMutation { writeTest { test } }" results, params = run_http_query(schema, "post", {}, query_data=dict(query=query)) - assert as_dicts(results) == [{"data": {"writeTest": {"test": "Hello World"}}}] + assert results == [({"writeTest": {"test": "Hello World"}}, None)] def test_batch_allows_post_with_json_encoding(): data = load_json_body('[{"query": "{test}"}]') results, params = run_http_query(schema, "post", data, batch_enabled=True) - assert as_dicts(results) == [{"data": {"test": "Hello World"}}] + assert results == [({"test": "Hello World"}, None)] def test_batch_supports_post_json_query_with_json_variables(): @@ -505,7 +551,7 @@ def test_batch_supports_post_json_query_with_json_variables(): ) results, params = run_http_query(schema, "post", data, batch_enabled=True) - assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + assert results == [({"test": "Hello Dolly"}, None)] def test_batch_allows_post_with_operation_name(): @@ -525,124 +571,4 @@ def test_batch_allows_post_with_operation_name(): data = load_json_body(json_encode(data)) results, params = run_http_query(schema, "post", data, batch_enabled=True) - assert as_dicts(results) == [ - {"data": {"test": "Hello World", "shared": "Hello Everyone"}} - ] - - -def test_get_responses_using_executor(): - class TestExecutor(object): - called = False - waited = False - cleaned = False - - def wait_until_finished(self): - TestExecutor.waited = True - - def clean(self): - TestExecutor.cleaned = True - - def execute(self, fn, *args, **kwargs): - TestExecutor.called = True - return fn(*args, **kwargs) - - query = "{test}" - results, params = run_http_query( - schema, "get", {}, dict(query=query), executor=TestExecutor(), - ) - - assert isinstance(results, list) - assert len(results) == 1 - assert isinstance(results[0], ExecutionResult) - - assert as_dicts(results) == [{"data": {"test": "Hello World"}}] - assert params == [RequestParams(query=query, variables=None, operation_name=None)] - assert TestExecutor.called - assert TestExecutor.waited - assert not TestExecutor.cleaned - - -def test_get_responses_using_executor_return_promise(): - class TestExecutor(object): - called = False - waited = False - cleaned = False - - def wait_until_finished(self): - TestExecutor.waited = True - - def clean(self): - TestExecutor.cleaned = True - - def execute(self, fn, *args, **kwargs): - TestExecutor.called = True - return fn(*args, **kwargs) - - query = "{test}" - result_promises, params = run_http_query( - schema, - "get", - {}, - dict(query=query), - executor=TestExecutor(), - return_promise=True, - ) - - assert isinstance(result_promises, list) - assert len(result_promises) == 1 - assert isinstance(result_promises[0], Promise) - results = Promise.all(result_promises).get() - - assert as_dicts(results) == [{"data": {"test": "Hello World"}}] - assert params == [RequestParams(query=query, variables=None, operation_name=None)] - assert TestExecutor.called - assert not TestExecutor.waited - assert TestExecutor.cleaned - - -def test_syntax_error_using_executor_return_promise(): - class TestExecutor(object): - called = False - waited = False - cleaned = False - - def wait_until_finished(self): - TestExecutor.waited = True - - def clean(self): - TestExecutor.cleaned = True - - def execute(self, fn, *args, **kwargs): - TestExecutor.called = True - return fn(*args, **kwargs) - - query = "this is a syntax error" - result_promises, params = run_http_query( - schema, - "get", - {}, - dict(query=query), - executor=TestExecutor(), - return_promise=True, - ) - - assert isinstance(result_promises, list) - assert len(result_promises) == 1 - assert isinstance(result_promises[0], Promise) - results = Promise.all(result_promises).get() - - assert isinstance(results, list) - assert len(results) == 1 - result = results[0] - assert isinstance(result, ExecutionResult) - - assert result.data is None - assert isinstance(result.errors, list) - assert len(result.errors) == 1 - error = result.errors[0] - assert isinstance(error, GraphQLSyntaxError) - - assert params == [RequestParams(query=query, variables=None, operation_name=None)] - assert not TestExecutor.called - assert not TestExecutor.waited - assert not TestExecutor.cleaned + assert results == [({"test": "Hello World", "shared": "Hello Everyone"}, None)] diff --git a/tests/utils.py b/tests/utils.py index 136f09f..1eafca0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,12 @@ -def as_dicts(results): +from typing import List + +from graphql import ExecutionResult + + +def as_dicts(results: List[ExecutionResult]): """Convert execution results to a list of tuples of dicts for better comparison.""" - return [result.to_dict(dict_class=dict) for result in results] + return [ + { + "data": result.data, + "errors": [error.formatted for error in result.errors] if result.errors else result.errors + } for result in results] From f6a2e46f2218e973b59c9d81729f25c0a610ae42 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Mon, 16 Mar 2020 23:24:33 -0500 Subject: [PATCH 04/17] chore: apply black formatting --- graphql_server/__init__.py | 47 +++++++++++++++++++++++++++----------- tests/schema.py | 4 ++-- tests/test_helpers.py | 10 ++++++-- tests/test_query.py | 21 +++++++++-------- tests/utils.py | 8 +++++-- 5 files changed, 62 insertions(+), 28 deletions(-) diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index 6a018bc..aaab794 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -13,8 +13,16 @@ from collections.abc import MutableMapping from typing import Optional, List, Callable, Dict, Any, Union, Type -from graphql import (GraphQLSchema, ExecutionResult, GraphQLError, parse, get_operation_ast, - validate_schema, validate, execute) +from graphql import ( + GraphQLSchema, + ExecutionResult, + GraphQLError, + parse, + get_operation_ast, + validate_schema, + validate, + execute, +) from graphql import format_error as format_error_default from promise import promisify, Promise @@ -31,7 +39,7 @@ "GraphQLParams", "GraphQLResponse", "ServerResponse", - "format_execution_result" + "format_execution_result", ] @@ -52,7 +60,7 @@ def run_http_query( query_data: Optional[Dict] = None, batch_enabled: bool = False, catch: bool = False, - **execute_options: Dict[str, Any] + **execute_options: Dict[str, Any], ) -> GraphQLResponse: """Execute GraphQL coming from an HTTP query against a given schema. @@ -103,9 +111,14 @@ def run_http_query( if not is_batch: extra_data = query_data or {} - all_params: List[GraphQLParams] = [get_graphql_params(entry, extra_data) for entry in data] + all_params: List[GraphQLParams] = [ + get_graphql_params(entry, extra_data) for entry in data + ] - results = [get_response(schema, params, catch_exc, allow_only_query, **execute_options) for params in all_params] + results = [ + get_response(schema, params, catch_exc, allow_only_query, **execute_options) + for params in all_params + ] return GraphQLResponse(results, all_params) @@ -209,7 +222,7 @@ def execute_graphql_request( schema: GraphQLSchema, params: GraphQLParams, allow_only_query: bool = False, - **kwargs + **kwargs, ) -> ExecutionResult: """Execute a GraphQL request and return an ExecutionResult. @@ -242,7 +255,7 @@ def execute_graphql_request( operation_ast = get_operation_ast(document, params.operation_name) if operation_ast: operation = operation_ast.operation.value - if operation != 'query': + if operation != "query": raise HttpQueryError( 405, f"Can only perform a {operation} operation from a POST request.", @@ -253,7 +266,13 @@ def execute_graphql_request( if validation_errors: return ExecutionResult(data=None, errors=validation_errors) - return execute(schema, document, variable_values=params.variables, operation_name=params.operation_name, **kwargs) + return execute( + schema, + document, + variable_values=params.variables, + operation_name=params.operation_name, + **kwargs, + ) @promisify @@ -266,7 +285,7 @@ def get_response( params: GraphQLParams, catch_exc: Type[BaseException], allow_only_query: bool = False, - **kwargs + **kwargs, ) -> Optional[Union[ExecutionResult, Promise[ExecutionResult]]]: """Get an individual execution result as response, with option to catch errors. @@ -274,7 +293,9 @@ def get_response( that belong to an exception class that you need to pass as a parameter. """ # Note: PyCharm will display a error due to the triple dot being used on Callable. - execute_request: Callable[..., Union[Promise[ExecutionResult], ExecutionResult]] = execute_graphql_request + execute_request: Callable[ + ..., Union[Promise[ExecutionResult], ExecutionResult] + ] = execute_graphql_request if kwargs.get("return_promise", False): execute_request = execute_graphql_request_as_promise @@ -289,7 +310,7 @@ def get_response( def format_execution_result( execution_result: Optional[ExecutionResult], - format_error: Optional[Callable[[Exception], Dict]] = format_error_default + format_error: Optional[Callable[[Exception], Dict]] = format_error_default, ) -> GraphQLResponse: """Format an execution result into a GraphQLResponse. @@ -304,6 +325,6 @@ def format_execution_result( response = {"errors": [format_error(e) for e in execution_result.errors]} status_code = 400 else: - response = {'data': execution_result.data} + response = {"data": execution_result.data} return FormattedResult(response, status_code) diff --git a/tests/schema.py b/tests/schema.py index d8d7827..bffc1f9 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -13,7 +13,7 @@ def resolve_thrower(*_args): def resolve_request(_obj, info): - return info.context.get('q') + return info.context.get("q") def resolve_context(_obj, info): @@ -41,4 +41,4 @@ def resolve_context(_obj, info): }, ) -schema = GraphQLSchema(QueryRootType, MutationRootType) \ No newline at end of file +schema = GraphQLSchema(QueryRootType, MutationRootType) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 5710b03..e7a90ad 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -82,7 +82,10 @@ def test_encode_execution_results_with_error(): None, [ GraphQLError( - "Some error", source=Source(body="Some error"), positions=[1], path=["somePath"] + "Some error", + source=Source(body="Some error"), + positions=[1], + path=["somePath"], ) ], ), @@ -122,7 +125,10 @@ def test_encode_execution_results_with_format_error(): None, [ GraphQLError( - "Some msg", source=Source("Some msg"), positions=[1], path=["some", "path"] + "Some msg", + source=Source("Some msg"), + positions=[1], + path=["some", "path"], ) ], ) diff --git a/tests/test_query.py b/tests/test_query.py index e0c26e8..3823101 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -13,7 +13,7 @@ load_json_body, run_http_query, format_execution_result, - encode_execution_results + encode_execution_results, ) from .schema import schema @@ -72,7 +72,9 @@ def test_allows_get_with_operation_name(): ), ) - assert as_dicts(results) == [{"data": {"test": "Hello World", "shared": "Hello Everyone"}, "errors": None}] + assert as_dicts(results) == [ + {"data": {"test": "Hello World", "shared": "Hello Everyone"}, "errors": None} + ] def test_reports_validation_errors(): @@ -87,14 +89,14 @@ def test_reports_validation_errors(): { "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], - "path": None + "path": None, }, { "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 21}], - "path": None + "path": None, }, - ] + ], } ] @@ -141,9 +143,9 @@ def test_errors_when_missing_operation_name(): "Must provide operation name" " if query contains multiple operations." ), - "path": None + "path": None, } - ] + ], } ] assert isinstance(results[0].errors[0], GraphQLError) @@ -407,7 +409,7 @@ def test_handles_errors_caused_by_a_lack_of_query(): def test_handles_errors_caused_by_invalid_query_type(): results, params = run_http_query(schema, "get", dict(query=42)) - assert results == [(None, [{'message': 'Must provide Source. Received: 42'}])] + assert results == [(None, [{"message": "Must provide Source. Received: 42"}])] def test_handles_batch_correctly_if_is_disabled(): @@ -454,7 +456,8 @@ def test_handles_bad_schema(): run_http_query("not a schema", "get", {}) # type: ignore assert str(exc_info.value) == ( - "Expected a GraphQL schema, but received 'not a schema'.") + "Expected a GraphQL schema, but received 'not a schema'." + ) def test_handles_unsupported_http_methods(): diff --git a/tests/utils.py b/tests/utils.py index 1eafca0..895c777 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,5 +8,9 @@ def as_dicts(results: List[ExecutionResult]): return [ { "data": result.data, - "errors": [error.formatted for error in result.errors] if result.errors else result.errors - } for result in results] + "errors": [error.formatted for error in result.errors] + if result.errors + else result.errors, + } + for result in results + ] From a1d4ae9fa843feb9ad39f6997897e880bcc11289 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Mon, 16 Mar 2020 23:27:35 -0500 Subject: [PATCH 05/17] chore: fix flake8 issues --- graphql_server/__init__.py | 3 ++- tests/test_query.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index aaab794..0aef38c 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -236,7 +236,8 @@ def execute_graphql_request( if not params.query: raise HttpQueryError(400, "Must provide query string.") - # Validate the schema and return a list of errors if it does not satisfy the Type System. + # Validate the schema and return a list of errors if it + # does not satisfy the Type System. schema_validation_errors = validate_schema(schema) if schema_validation_errors: return ExecutionResult(data=None, errors=schema_validation_errors) diff --git a/tests/test_query.py b/tests/test_query.py index 3823101..3f7fb38 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,7 +1,6 @@ import json -from typing import List -from graphql.error import GraphQLError, GraphQLSyntaxError +from graphql.error import GraphQLError from graphql.execution import ExecutionResult from pytest import raises From 648e79c6985e3173adfaf479da3b9b990b369bb0 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Tue, 17 Mar 2020 19:35:15 -0500 Subject: [PATCH 06/17] chore: remove promise package --- graphql_server/__init__.py | 19 +++---------------- setup.py | 1 - 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index 0aef38c..4571840 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -25,8 +25,6 @@ ) from graphql import format_error as format_error_default -from promise import promisify, Promise - from .error import HttpQueryError @@ -60,7 +58,7 @@ def run_http_query( query_data: Optional[Dict] = None, batch_enabled: bool = False, catch: bool = False, - **execute_options: Dict[str, Any], + **execute_options, ) -> GraphQLResponse: """Execute GraphQL coming from an HTTP query against a given schema. @@ -276,33 +274,22 @@ def execute_graphql_request( ) -@promisify -def execute_graphql_request_as_promise(*args, **kwargs): - return execute_graphql_request(*args, **kwargs) - - def get_response( schema: GraphQLSchema, params: GraphQLParams, catch_exc: Type[BaseException], allow_only_query: bool = False, **kwargs, -) -> Optional[Union[ExecutionResult, Promise[ExecutionResult]]]: +) -> Optional[ExecutionResult]: """Get an individual execution result as response, with option to catch errors. This does the same as execute_graphql_request() except that you can catch errors that belong to an exception class that you need to pass as a parameter. """ - # Note: PyCharm will display a error due to the triple dot being used on Callable. - execute_request: Callable[ - ..., Union[Promise[ExecutionResult], ExecutionResult] - ] = execute_graphql_request - if kwargs.get("return_promise", False): - execute_request = execute_graphql_request_as_promise # noinspection PyBroadException try: - execution_result = execute_request(schema, params, allow_only_query, **kwargs) + execution_result = execute_graphql_request(schema, params, allow_only_query, **kwargs) except catch_exc: return None diff --git a/setup.py b/setup.py index fda8577..02fdaf8 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,6 @@ install_requires = [ "graphql-core>=3,<4", - "promise>=2.3,<3", ] tests_requires = [ From 6ce66be43a4b5f7fb77f978e98a89aaae593d22d Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Tue, 17 Mar 2020 19:35:33 -0500 Subject: [PATCH 07/17] tests: achieve 100% coverage --- tests/schema.py | 1 + tests/test_query.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/schema.py b/tests/schema.py index bffc1f9..5aae6f7 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -42,3 +42,4 @@ def resolve_context(_obj, info): ) schema = GraphQLSchema(QueryRootType, MutationRootType) +invalid_schema = GraphQLSchema() diff --git a/tests/test_query.py b/tests/test_query.py index 3f7fb38..0f30557 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -15,7 +15,7 @@ encode_execution_results, ) -from .schema import schema +from .schema import schema, invalid_schema from .utils import as_dicts @@ -31,6 +31,23 @@ def test_server_results(): assert GraphQLResponse._fields == ("results", "params") +def test_validate_schema(): + query = "{test}" + results, params = run_http_query(invalid_schema, "get", {}, dict(query=query)) + assert as_dicts(results) == [ + { + "data": None, + "errors": [ + { + "locations": None, + "message": "Query root type must be provided.", + "path": None + } + ] + } + ] + + def test_allows_get_with_query_param(): query = "{test}" results, params = run_http_query(schema, "get", {}, dict(query=query)) From 58feca8fda73a3d553b6dda1b4765f951b77545a Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Tue, 17 Mar 2020 19:49:59 -0500 Subject: [PATCH 08/17] chore: apply compatible isort-black options --- graphql_server/__init__.py | 21 +++++++-------------- setup.cfg | 4 ++++ tests/schema.py | 2 +- tests/test_query.py | 12 ++++++------ 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index 4571840..a2d4956 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -11,23 +11,14 @@ import json from collections import namedtuple from collections.abc import MutableMapping -from typing import Optional, List, Callable, Dict, Any, Union, Type - -from graphql import ( - GraphQLSchema, - ExecutionResult, - GraphQLError, - parse, - get_operation_ast, - validate_schema, - validate, - execute, -) +from typing import Any, Callable, Dict, List, Optional, Type, Union + +from graphql import ExecutionResult, GraphQLError, GraphQLSchema, execute from graphql import format_error as format_error_default +from graphql import get_operation_ast, parse, validate, validate_schema from .error import HttpQueryError - __all__ = [ "run_http_query", "encode_execution_results", @@ -289,7 +280,9 @@ def get_response( # noinspection PyBroadException try: - execution_result = execute_graphql_request(schema, params, allow_only_query, **kwargs) + execution_result = execute_graphql_request( + schema, params, allow_only_query, **kwargs + ) except catch_exc: return None diff --git a/setup.cfg b/setup.cfg index 70e1f4a..126b454 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,6 +4,10 @@ max-line-length = 88 [isort] known_first_party=graphql_server +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True [tool:pytest] norecursedirs = venv .venv .tox .git .cache .mypy_cache .pytest_cache diff --git a/tests/schema.py b/tests/schema.py index 5aae6f7..c7665ba 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -3,8 +3,8 @@ GraphQLField, GraphQLNonNull, GraphQLObjectType, - GraphQLString, GraphQLSchema, + GraphQLString, ) diff --git a/tests/test_query.py b/tests/test_query.py index 0f30557..eb63ef7 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -5,17 +5,17 @@ from pytest import raises from graphql_server import ( - HttpQueryError, GraphQLParams, GraphQLResponse, + HttpQueryError, + encode_execution_results, + format_execution_result, json_encode, load_json_body, run_http_query, - format_execution_result, - encode_execution_results, ) -from .schema import schema, invalid_schema +from .schema import invalid_schema, schema from .utils import as_dicts @@ -41,9 +41,9 @@ def test_validate_schema(): { "locations": None, "message": "Query root type must be provided.", - "path": None + "path": None, } - ] + ], } ] From 7a19c8ef109363bfe24a692497ccc96cbf34d9d9 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Tue, 17 Mar 2020 20:09:18 -0500 Subject: [PATCH 09/17] chore: solve dev tools issues --- graphql_server/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index a2d4956..f3eeb40 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -11,7 +11,7 @@ import json from collections import namedtuple from collections.abc import MutableMapping -from typing import Any, Callable, Dict, List, Optional, Type, Union +from typing import Any, Awaitable, Callable, Dict, List, Optional, Type, Union from graphql import ExecutionResult, GraphQLError, GraphQLSchema, execute from graphql import format_error as format_error_default @@ -125,7 +125,7 @@ def json_encode(data: Union[Dict, List], pretty: bool = False) -> str: def encode_execution_results( execution_results: List[Optional[ExecutionResult]], - format_error: Callable[[Exception], Dict] = format_error_default, + format_error: Callable[[GraphQLError], Dict] = format_error_default, is_batch: bool = False, encode: Callable[[Dict], Any] = json_encode, ) -> ServerResponse: @@ -212,7 +212,7 @@ def execute_graphql_request( params: GraphQLParams, allow_only_query: bool = False, **kwargs, -) -> ExecutionResult: +) -> Union[Awaitable[ExecutionResult], ExecutionResult]: """Execute a GraphQL request and return an ExecutionResult. You need to pass the GraphQL schema and the GraphQLParams that you can get @@ -271,7 +271,7 @@ def get_response( catch_exc: Type[BaseException], allow_only_query: bool = False, **kwargs, -) -> Optional[ExecutionResult]: +) -> Optional[Union[Awaitable[ExecutionResult], ExecutionResult]]: """Get an individual execution result as response, with option to catch errors. This does the same as execute_graphql_request() except that you can catch errors @@ -291,8 +291,8 @@ def get_response( def format_execution_result( execution_result: Optional[ExecutionResult], - format_error: Optional[Callable[[Exception], Dict]] = format_error_default, -) -> GraphQLResponse: + format_error: Optional[Callable[[GraphQLError], Dict]] = format_error_default, +) -> FormattedResult: """Format an execution result into a GraphQLResponse. This converts the given execution result into a FormattedResult that contains @@ -303,7 +303,8 @@ def format_execution_result( if execution_result: if execution_result.errors: - response = {"errors": [format_error(e) for e in execution_result.errors]} + fe = [format_error(e) for e in execution_result.errors] # type: ignore + response = {"errors": fe} status_code = 400 else: response = {"data": execution_result.data} From 32f19c1a10332e664b612c109e579edf903c3494 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Tue, 17 Mar 2020 20:09:36 -0500 Subject: [PATCH 10/17] chore: remove pypy3 from tox envlist --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 16b4fd0..2453c8b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = black,flake8,import-order,mypy,manifest, - py{36,37,38,39-dev,py3} + py{36,37,38,39-dev} ; requires = tox-conda [testenv] From 0bc2f45801a09d6bd59c40869d1021bfceb5f0d4 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Tue, 17 Mar 2020 20:11:54 -0500 Subject: [PATCH 11/17] chore: remove pypy3 from travis --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ee93dd9..29bac19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ python: - 3.7 - 3.8 - 3.9-dev - - pypy3 matrix: include: - python: 3.7 From ea4b882a79cf75eab607660895487fa9a998a36b Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Mon, 6 Apr 2020 17:40:21 -0500 Subject: [PATCH 12/17] tests: re-add helper tests --- README.md | 2 ++ graphql_server/__init__.py | 5 +++++ tests/test_helpers.py | 17 +++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/README.md b/README.md index fdb3d40..9e228f1 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ The `graphql_server` package provides these public helper functions: * `json_encode` * `json_encode_pretty` +**NOTE:** the `json_encode_pretty` is kept as backward compatibility change as it uses `json_encode` with `pretty` parameter set to `True`. + All functions in the package are annotated with type hints and docstrings, and you can build HTML documentation from these using `bin/build_docs`. diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index f3eeb40..ca465b2 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -24,6 +24,7 @@ "encode_execution_results", "load_json_body", "json_encode", + "json_encode_pretty", "HttpQueryError", "GraphQLParams", "GraphQLResponse", @@ -123,6 +124,10 @@ def json_encode(data: Union[Dict, List], pretty: bool = False) -> str: return json.dumps(data, indent=2, separators=(",", ": ")) +def json_encode_pretty(data: Union[Dict, List]) -> str: + return json_encode(data, True) + + def encode_execution_results( execution_results: List[Optional[ExecutionResult]], format_error: Callable[[GraphQLError], Dict] = format_error_default, diff --git a/tests/test_helpers.py b/tests/test_helpers.py index e7a90ad..d2c6b50 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -10,6 +10,7 @@ ServerResponse, encode_execution_results, json_encode, + json_encode_pretty, load_json_body, ) @@ -201,3 +202,19 @@ def encode(result): assert isinstance(output.status_code, int) assert output.body == "{'data': {'result': None}}" assert output.status_code == 200 + + +def test_encode_execution_results_with_pretty_encode(): + execution_results = [ExecutionResult({"test": "Hello World"}, None)] + + output = encode_execution_results(execution_results, encode=json_encode_pretty) + body = output.body + assert body == "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" + + +def test_encode_execution_results_not_pretty_by_default(): + execution_results = [ExecutionResult({"test": "Hello World"}, None)] + # execution_results = [ExecutionResult({"result": None}, None)] + + output = encode_execution_results(execution_results) + assert output.body == '{"data":{"test":"Hello World"}}' From c3cb505a3c715d14b7b53bd84833c997425713d4 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Tue, 7 Apr 2020 19:45:56 -0500 Subject: [PATCH 13/17] chore: pin graphql-core to 3.1.0 --- setup.py | 2 +- tests/test_query.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 02fdaf8..b4ff17d 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages install_requires = [ - "graphql-core>=3,<4", + "graphql-core==3.1.0", ] tests_requires = [ diff --git a/tests/test_query.py b/tests/test_query.py index eb63ef7..4a713ae 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -408,7 +408,7 @@ def test_handles_syntax_errors_caught_by_graphql(): [ { "locations": [(1, 1)], - "message": "Syntax Error: Unexpected Name 'syntaxerror'", + "message": "Syntax Error: Unexpected Name 'syntaxerror'.", } ], ) @@ -425,7 +425,7 @@ def test_handles_errors_caused_by_a_lack_of_query(): def test_handles_errors_caused_by_invalid_query_type(): results, params = run_http_query(schema, "get", dict(query=42)) - assert results == [(None, [{"message": "Must provide Source. Received: 42"}])] + assert results == [(None, [{"message": "Must provide Source. Received: 42."}])] def test_handles_batch_correctly_if_is_disabled(): From 99c40ec15db4fff77ef5ff40ad6fbe6412aba67e Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Wed, 8 Apr 2020 22:11:15 -0500 Subject: [PATCH 14/17] refactor: use graphql and graphql-sync functions --- graphql_server/__init__.py | 127 ++++++++++++++++--------------------- setup.cfg | 1 + setup.py | 1 + tests/test_asyncio.py | 74 +++++++++++++++++++++ tests/test_query.py | 1 - 5 files changed, 131 insertions(+), 73 deletions(-) create mode 100644 tests/test_asyncio.py diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index ca465b2..29efffa 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -6,16 +6,16 @@ for building GraphQL servers or integrations into existing web frameworks using [GraphQL-Core](https://github.com/graphql-python/graphql-core). """ - - import json from collections import namedtuple from collections.abc import MutableMapping -from typing import Any, Awaitable, Callable, Dict, List, Optional, Type, Union +from typing import Any, Callable, Dict, List, Optional, Type, Union -from graphql import ExecutionResult, GraphQLError, GraphQLSchema, execute +from graphql import ExecutionResult, GraphQLError, GraphQLSchema, OperationType from graphql import format_error as format_error_default -from graphql import get_operation_ast, parse, validate, validate_schema +from graphql import get_operation_ast, parse +from graphql.graphql import graphql, graphql_sync +from graphql.pyutils import AwaitableOrValue from .error import HttpQueryError @@ -50,6 +50,7 @@ def run_http_query( query_data: Optional[Dict] = None, batch_enabled: bool = False, catch: bool = False, + run_sync: bool = True, **execute_options, ) -> GraphQLResponse: """Execute GraphQL coming from an HTTP query against a given schema. @@ -105,11 +106,12 @@ def run_http_query( get_graphql_params(entry, extra_data) for entry in data ] - results = [ - get_response(schema, params, catch_exc, allow_only_query, **execute_options) + results: List[Optional[AwaitableOrValue[ExecutionResult]]] = [ + get_response( + schema, params, catch_exc, allow_only_query, run_sync, **execute_options + ) for params in all_params ] - return GraphQLResponse(results, all_params) @@ -212,82 +214,63 @@ def load_json_variables(variables: Optional[Union[str, Dict]]) -> Optional[Dict] return variables # type: ignore -def execute_graphql_request( - schema: GraphQLSchema, - params: GraphQLParams, - allow_only_query: bool = False, - **kwargs, -) -> Union[Awaitable[ExecutionResult], ExecutionResult]: - """Execute a GraphQL request and return an ExecutionResult. - - You need to pass the GraphQL schema and the GraphQLParams that you can get - with the get_graphql_params() function. If you only want to allow GraphQL query - operations, then set allow_only_query=True. You can also specify a custom - GraphQLBackend instance that shall be used by GraphQL-Core instead of the - default one. All other keyword arguments are passed on to the GraphQL-Core - function for executing GraphQL queries. - """ - if not params.query: - raise HttpQueryError(400, "Must provide query string.") - - # Validate the schema and return a list of errors if it - # does not satisfy the Type System. - schema_validation_errors = validate_schema(schema) - if schema_validation_errors: - return ExecutionResult(data=None, errors=schema_validation_errors) - - # Parse the query and return ExecutionResult with errors found. - # Any Exception is parsed as GraphQLError. - try: - document = parse(params.query) - except GraphQLError as e: - return ExecutionResult(data=None, errors=[e]) - except Exception as e: - e = GraphQLError(str(e), original_error=e) - return ExecutionResult(data=None, errors=[e]) - - if allow_only_query: - operation_ast = get_operation_ast(document, params.operation_name) - if operation_ast: - operation = operation_ast.operation.value - if operation != "query": - raise HttpQueryError( - 405, - f"Can only perform a {operation} operation from a POST request.", - headers={"Allow": "POST"}, - ) - - validation_errors = validate(schema, document) - if validation_errors: - return ExecutionResult(data=None, errors=validation_errors) - - return execute( - schema, - document, - variable_values=params.variables, - operation_name=params.operation_name, - **kwargs, - ) - - def get_response( schema: GraphQLSchema, params: GraphQLParams, catch_exc: Type[BaseException], allow_only_query: bool = False, + run_sync: bool = True, **kwargs, -) -> Optional[Union[Awaitable[ExecutionResult], ExecutionResult]]: +) -> Optional[AwaitableOrValue[ExecutionResult]]: """Get an individual execution result as response, with option to catch errors. - This does the same as execute_graphql_request() except that you can catch errors - that belong to an exception class that you need to pass as a parameter. + This does the same as graphql_impl() except that you can either + throw an error on the ExecutionResult if allow_only_query is set to True + or catch errors that belong to an exception class that you need to pass + as a parameter. """ + if not params.query: + raise HttpQueryError(400, "Must provide query string.") + # noinspection PyBroadException try: - execution_result = execute_graphql_request( - schema, params, allow_only_query, **kwargs - ) + # Parse document to trigger a new HttpQueryError if allow_only_query is True + try: + document = parse(params.query) + except GraphQLError as e: + return ExecutionResult(data=None, errors=[e]) + except Exception as e: + e = GraphQLError(str(e), original_error=e) + return ExecutionResult(data=None, errors=[e]) + + if allow_only_query: + operation_ast = get_operation_ast(document, params.operation_name) + if operation_ast: + operation = operation_ast.operation.value + if operation != OperationType.QUERY.value: + raise HttpQueryError( + 405, + f"Can only perform a {operation} operation from a POST request.", # noqa + headers={"Allow": "POST"}, + ) + + if run_sync: + execution_result = graphql_sync( + schema=schema, + source=params.query, + variable_values=params.variables, + operation_name=params.operation_name, + **kwargs, + ) + else: + execution_result = graphql( # type: ignore + schema=schema, + source=params.query, + variable_values=params.variables, + operation_name=params.operation_name, + **kwargs, + ) except catch_exc: return None diff --git a/setup.cfg b/setup.cfg index 126b454..78bddbd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,6 +11,7 @@ use_parentheses=True [tool:pytest] norecursedirs = venv .venv .tox .git .cache .mypy_cache .pytest_cache +markers = asyncio [bdist_wheel] universal=1 diff --git a/setup.py b/setup.py index b4ff17d..dd022c6 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ tests_requires = [ "pytest>=5.3,<5.4", "pytest-cov>=2.8,<3", + "pytest-asyncio>=0.10,<1" ] dev_requires = [ diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py new file mode 100644 index 0000000..0471087 --- /dev/null +++ b/tests/test_asyncio.py @@ -0,0 +1,74 @@ +import asyncio + +from graphql.type.definition import ( + GraphQLField, + GraphQLNonNull, + GraphQLObjectType, +) +from graphql.type.scalars import GraphQLString +from graphql.type.schema import GraphQLSchema +from promise import Promise +from pytest import mark + +from graphql_server import GraphQLParams, run_http_query + +from .utils import as_dicts + + +def resolve_error_sync(_obj, _info): + raise ValueError("error sync") + + +async def resolve_error_async(_obj, _info): + await asyncio.sleep(0.001) + raise ValueError("error async") + + +def resolve_field_sync(_obj, _info): + return "sync" + + +async def resolve_field_async(_obj, info): + await asyncio.sleep(0.001) + return "async" + + +NonNullString = GraphQLNonNull(GraphQLString) + +QueryRootType = GraphQLObjectType( + name="QueryRoot", + fields={ + "errorSync": GraphQLField(NonNullString, resolve=resolve_error_sync), + "errorAsync": GraphQLField(NonNullString, resolve=resolve_error_async), + "fieldSync": GraphQLField(NonNullString, resolve=resolve_field_sync), + "fieldAsync": GraphQLField(NonNullString, resolve=resolve_field_async), + }, +) + +schema = GraphQLSchema(QueryRootType) + + +@mark.asyncio +def test_get_responses_using_asyncio_executor(): + query = "{fieldSync fieldAsync}" + + loop = asyncio.get_event_loop() + + async def get_results(): + result_promises, params = run_http_query( + schema, "get", {}, dict(query=query), run_sync=False + ) + results = await Promise.all(result_promises) + return results, params + + try: + results, params = loop.run_until_complete(get_results()) + finally: + loop.close() + + expected_results = [ + {"data": {"fieldSync": "sync", "fieldAsync": "async"}, "errors": None} + ] + + assert as_dicts(results) == expected_results + assert params == [GraphQLParams(query=query, variables=None, operation_name=None)] diff --git a/tests/test_query.py b/tests/test_query.py index 4a713ae..5e9618c 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -424,7 +424,6 @@ def test_handles_errors_caused_by_a_lack_of_query(): def test_handles_errors_caused_by_invalid_query_type(): results, params = run_http_query(schema, "get", dict(query=42)) - assert results == [(None, [{"message": "Must provide Source. Received: 42."}])] From 928ace8b93c044b88cd634ef7c3032fc386a9b9b Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Wed, 8 Apr 2020 22:26:10 -0500 Subject: [PATCH 15/17] tests: remove Promise and use async await iterator --- tests/test_asyncio.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index 0471087..fd3e4f9 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -7,7 +7,6 @@ ) from graphql.type.scalars import GraphQLString from graphql.type.schema import GraphQLSchema -from promise import Promise from pytest import mark from graphql_server import GraphQLParams, run_http_query @@ -58,7 +57,7 @@ async def get_results(): result_promises, params = run_http_query( schema, "get", {}, dict(query=query), run_sync=False ) - results = await Promise.all(result_promises) + results = [await result for result in result_promises] return results, params try: From f22d3969df422f363839702d3be4da50aef49212 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Wed, 8 Apr 2020 22:56:49 -0500 Subject: [PATCH 16/17] refactor: remove pytest-asyncio --- setup.py | 1 - tests/test_asyncio.py | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index dd022c6..b4ff17d 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,6 @@ tests_requires = [ "pytest>=5.3,<5.4", "pytest-cov>=2.8,<3", - "pytest-asyncio>=0.10,<1" ] dev_requires = [ diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index fd3e4f9..e07a2f8 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -7,7 +7,6 @@ ) from graphql.type.scalars import GraphQLString from graphql.type.schema import GraphQLSchema -from pytest import mark from graphql_server import GraphQLParams, run_http_query @@ -47,7 +46,6 @@ async def resolve_field_async(_obj, info): schema = GraphQLSchema(QueryRootType) -@mark.asyncio def test_get_responses_using_asyncio_executor(): query = "{fieldSync fieldAsync}" @@ -57,8 +55,8 @@ async def get_results(): result_promises, params = run_http_query( schema, "get", {}, dict(query=query), run_sync=False ) - results = [await result for result in result_promises] - return results, params + res = [await result for result in result_promises] + return res, params try: results, params = loop.run_until_complete(get_results()) From bed7432a20686e695b5c47e47c30512cd67db3ef Mon Sep 17 00:00:00 2001 From: Manuel Bojato <30560560+KingDarBoja@users.noreply.github.com> Date: Sun, 12 Apr 2020 11:48:45 -0500 Subject: [PATCH 17/17] chore: set graphql-core dependency semver Co-Authored-By: Jonathan Kim --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b4ff17d..2bedab1 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages install_requires = [ - "graphql-core==3.1.0", + "graphql-core>=3.1.0,<4", ] tests_requires = [