From 476edf370099df050289f9c0b8d70007e7dc8ecc Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Fri, 24 Dec 2021 14:28:48 +0100 Subject: [PATCH 01/20] Accept Graphene wrapped GraphQL schemas --- graphql_server/aiohttp/graphqlview.py | 8 +++++--- graphql_server/flask/graphqlview.py | 8 +++++--- graphql_server/quart/graphqlview.py | 8 +++++--- graphql_server/sanic/graphqlview.py | 8 +++++--- graphql_server/webob/graphqlview.py | 8 +++++--- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/graphql_server/aiohttp/graphqlview.py b/graphql_server/aiohttp/graphqlview.py index 0081174..deb6522 100644 --- a/graphql_server/aiohttp/graphqlview.py +++ b/graphql_server/aiohttp/graphqlview.py @@ -56,9 +56,11 @@ def __init__(self, **kwargs): if hasattr(self, key): setattr(self, key, value) - assert isinstance( - self.schema, GraphQLSchema - ), "A Schema is required to be provided to GraphQLView." + if not isinstance(self.schema, GraphQLSchema): + # maybe the GraphQL schema is wrapped in a Graphene schema + self.schema = getattr(self.schema, "graphql_schema", None) + if not isinstance(self.schema, GraphQLSchema): + raise TypeError("A Schema is required to be provided to GraphQLView.") def get_root_value(self): return self.root_value diff --git a/graphql_server/flask/graphqlview.py b/graphql_server/flask/graphqlview.py index 59097d9..2a9e451 100644 --- a/graphql_server/flask/graphqlview.py +++ b/graphql_server/flask/graphqlview.py @@ -55,9 +55,11 @@ def __init__(self, **kwargs): if hasattr(self, key): setattr(self, key, value) - assert isinstance( - self.schema, GraphQLSchema - ), "A Schema is required to be provided to GraphQLView." + if not isinstance(self.schema, GraphQLSchema): + # maybe the GraphQL schema is wrapped in a Graphene schema + self.schema = getattr(self.schema, "graphql_schema", None) + if not isinstance(self.schema, GraphQLSchema): + raise TypeError("A Schema is required to be provided to GraphQLView.") def get_root_value(self): return self.root_value diff --git a/graphql_server/quart/graphqlview.py b/graphql_server/quart/graphqlview.py index 3f01edc..ff737ec 100644 --- a/graphql_server/quart/graphqlview.py +++ b/graphql_server/quart/graphqlview.py @@ -57,9 +57,11 @@ def __init__(self, **kwargs): if hasattr(self, key): setattr(self, key, value) - assert isinstance( - self.schema, GraphQLSchema - ), "A Schema is required to be provided to GraphQLView." + if not isinstance(self.schema, GraphQLSchema): + # maybe the GraphQL schema is wrapped in a Graphene schema + self.schema = getattr(self.schema, "graphql_schema", None) + if not isinstance(self.schema, GraphQLSchema): + raise TypeError("A Schema is required to be provided to GraphQLView.") def get_root_value(self): return self.root_value diff --git a/graphql_server/sanic/graphqlview.py b/graphql_server/sanic/graphqlview.py index e184143..c7a3b75 100644 --- a/graphql_server/sanic/graphqlview.py +++ b/graphql_server/sanic/graphqlview.py @@ -58,9 +58,11 @@ def __init__(self, **kwargs): if hasattr(self, key): setattr(self, key, value) - assert isinstance( - self.schema, GraphQLSchema - ), "A Schema is required to be provided to GraphQLView." + if not isinstance(self.schema, GraphQLSchema): + # maybe the GraphQL schema is wrapped in a Graphene schema + self.schema = getattr(self.schema, "graphql_schema", None) + if not isinstance(self.schema, GraphQLSchema): + raise TypeError("A Schema is required to be provided to GraphQLView.") def get_root_value(self): return self.root_value diff --git a/graphql_server/webob/graphqlview.py b/graphql_server/webob/graphqlview.py index ba54599..0aa08c6 100644 --- a/graphql_server/webob/graphqlview.py +++ b/graphql_server/webob/graphqlview.py @@ -55,9 +55,11 @@ def __init__(self, **kwargs): if hasattr(self, key): setattr(self, key, value) - assert isinstance( - self.schema, GraphQLSchema - ), "A Schema is required to be provided to GraphQLView." + if not isinstance(self.schema, GraphQLSchema): + # maybe the GraphQL schema is wrapped in a Graphene schema + self.schema = getattr(self.schema, "graphql_schema", None) + if not isinstance(self.schema, GraphQLSchema): + raise TypeError("A Schema is required to be provided to GraphQLView.") def get_root_value(self): return self.root_value From 384ae78d257f0bb8bd86c581b7f01eb395378d6f Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Mon, 17 Jan 2022 11:52:54 +0100 Subject: [PATCH 02/20] Update GraphQL-core from 3.1 to 3.2 (#85) --- graphql_server/__init__.py | 6 +++++- setup.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index 239a1d4..5ae4acd 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -12,7 +12,6 @@ from typing import Any, Callable, Collection, Dict, List, Optional, Type, Union from graphql.error import GraphQLError -from graphql.error import format_error as format_error_default from graphql.execution import ExecutionResult, execute from graphql.language import OperationType, parse from graphql.pyutils import AwaitableOrValue @@ -55,6 +54,11 @@ # The public helper functions +def format_error_default(error: GraphQLError) -> Dict: + """The default function for converting GraphQLError to a dictionary.""" + return error.formatted + + def run_http_query( schema: GraphQLSchema, request_method: str, diff --git a/setup.py b/setup.py index e3f769e..91786c3 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from re import search from setuptools import setup, find_packages -install_requires = ["graphql-core>=3.1.0,<4", "typing-extensions>=3.7.4,<4"] +install_requires = ["graphql-core>=3.2,<3.3", "typing-extensions>=4,<5"] tests_requires = [ "pytest>=5.4,<5.5", From bc74eedab7e15b98aff4891dc1c74eb0528634f6 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Mon, 17 Jan 2022 13:12:26 +0100 Subject: [PATCH 03/20] Empty fields are not contained in formatted errors any more --- setup.py | 8 +-- tests/aiohttp/test_graphqlview.py | 86 ++++++++++++++----------------- tests/flask/test_graphqlview.py | 46 ++++++++--------- tests/quart/test_graphqlview.py | 52 ++++++------------- tests/sanic/test_graphqlview.py | 46 ++++++----------- tests/test_query.py | 13 +---- tests/webob/test_graphqlview.py | 52 ++++++------------- 7 files changed, 116 insertions(+), 187 deletions(-) diff --git a/setup.py b/setup.py index 91786c3..6bb761e 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,9 @@ ] dev_requires = [ - "flake8>=3.7,<4", - "isort>=4,<5", - "black==19.10b0", + "flake8>=4,<5", + "isort>=5,<6", + "black>=19.10b0", "mypy>=0.761,<0.770", "check-manifest>=0.40,<1", ] + tests_requires @@ -28,7 +28,7 @@ ] install_webob_requires = [ - "webob>=1.8.6,<2", + "webob>=1.8.7,<2", ] install_aiohttp_requires = [ diff --git a/tests/aiohttp/test_graphqlview.py b/tests/aiohttp/test_graphqlview.py index 0a940f9..815d23d 100644 --- a/tests/aiohttp/test_graphqlview.py +++ b/tests/aiohttp/test_graphqlview.py @@ -76,12 +76,10 @@ async def test_reports_validation_errors(client): { "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], - "path": None, }, { "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 21}], - "path": None, }, ], } @@ -107,8 +105,6 @@ async def test_errors_when_missing_operation_name(client): "Must provide operation name if query contains multiple " "operations." ), - "locations": None, - "path": None, }, ] } @@ -128,8 +124,6 @@ async def test_errors_when_sending_a_mutation_via_get(client): "errors": [ { "message": "Can only perform a mutation operation from a POST request.", - "locations": None, - "path": None, }, ], } @@ -152,8 +146,6 @@ async def test_errors_when_selecting_a_mutation_within_a_get(client): "errors": [ { "message": "Can only perform a mutation operation from a POST request.", - "locations": None, - "path": None, }, ], } @@ -174,10 +166,8 @@ async def test_errors_when_selecting_a_subscription_within_a_get(client): assert await response.json() == { "errors": [ { - "message": "Can only perform a subscription operation from a POST " - "request.", - "locations": None, - "path": None, + "message": "Can only perform a subscription operation" + " from a POST request.", }, ], } @@ -215,7 +205,11 @@ async def test_allows_post_with_json_encoding(client): async def test_allows_sending_a_mutation_via_post(client): response = await client.post( "/graphql", - data=json.dumps(dict(query="mutation TestMutation { writeTest { test } }",)), + data=json.dumps( + dict( + query="mutation TestMutation { writeTest { test } }", + ) + ), headers={"content-type": "application/json"}, ) @@ -292,7 +286,11 @@ async def test_supports_post_url_encoded_query_with_string_variables(client): async def test_supports_post_json_quey_with_get_variable_values(client): response = await client.post( url_string(variables=json.dumps({"who": "Dolly"})), - data=json.dumps(dict(query="query helloWho($who: String){ test(who: $who) }",)), + data=json.dumps( + dict( + query="query helloWho($who: String){ test(who: $who) }", + ) + ), headers={"content-type": "application/json"}, ) @@ -304,7 +302,11 @@ async def test_supports_post_json_quey_with_get_variable_values(client): async def test_post_url_encoded_query_with_get_variable_values(client): response = await client.post( url_string(variables=json.dumps({"who": "Dolly"})), - data=urlencode(dict(query="query helloWho($who: String){ test(who: $who) }",)), + data=urlencode( + dict( + query="query helloWho($who: String){ test(who: $who) }", + ) + ), headers={"content-type": "application/x-www-form-urlencoded"}, ) @@ -421,7 +423,6 @@ async def test_handles_syntax_errors_caught_by_graphql(client): { "locations": [{"column": 1, "line": 1}], "message": "Syntax Error: Unexpected Name 'syntaxerror'.", - "path": None, }, ], } @@ -433,16 +434,16 @@ async def test_handles_errors_caused_by_a_lack_of_query(client): assert response.status == 400 assert await response.json() == { - "errors": [ - {"message": "Must provide query string.", "locations": None, "path": None} - ] + "errors": [{"message": "Must provide query string."}] } @pytest.mark.asyncio async def test_handles_batch_correctly_if_is_disabled(client): response = await client.post( - "/graphql", data="[]", headers={"content-type": "application/json"}, + "/graphql", + data="[]", + headers={"content-type": "application/json"}, ) assert response.status == 400 @@ -450,8 +451,6 @@ async def test_handles_batch_correctly_if_is_disabled(client): "errors": [ { "message": "Batch GraphQL requests are not enabled.", - "locations": None, - "path": None, } ] } @@ -460,7 +459,9 @@ async def test_handles_batch_correctly_if_is_disabled(client): @pytest.mark.asyncio async def test_handles_incomplete_json_bodies(client): response = await client.post( - "/graphql", data='{"query":', headers={"content-type": "application/json"}, + "/graphql", + data='{"query":', + headers={"content-type": "application/json"}, ) assert response.status == 400 @@ -468,8 +469,6 @@ async def test_handles_incomplete_json_bodies(client): "errors": [ { "message": "POST body sent invalid JSON.", - "locations": None, - "path": None, } ] } @@ -484,9 +483,7 @@ async def test_handles_plain_post_text(client): ) assert response.status == 400 assert await response.json() == { - "errors": [ - {"message": "Must provide query string.", "locations": None, "path": None} - ] + "errors": [{"message": "Must provide query string."}] } @@ -499,9 +496,7 @@ async def test_handles_poorly_formed_variables(client): ) assert response.status == 400 assert await response.json() == { - "errors": [ - {"message": "Variables are invalid JSON.", "locations": None, "path": None} - ] + "errors": [{"message": "Variables are invalid JSON."}] } @@ -514,8 +509,6 @@ async def test_handles_unsupported_http_methods(client): "errors": [ { "message": "GraphQL only supports GET and POST requests.", - "locations": None, - "path": None, } ] } @@ -576,16 +569,15 @@ async def test_post_multipart_data(client): data = ( "------aiohttpgraphql\r\n" - + 'Content-Disposition: form-data; name="query"\r\n' - + "\r\n" - + query - + "\r\n" - + "------aiohttpgraphql--\r\n" - + "Content-Type: text/plain; charset=utf-8\r\n" - + 'Content-Disposition: form-data; name="file"; filename="text1.txt"; filename*=utf-8\'\'text1.txt\r\n' # noqa: ignore - + "\r\n" - + "\r\n" - + "------aiohttpgraphql--\r\n" + 'Content-Disposition: form-data; name="query"\r\n' + "\r\n" + query + "\r\n" + "------aiohttpgraphql--\r\n" + "Content-Type: text/plain; charset=utf-8\r\n" + 'Content-Disposition: form-data; name="file"; filename="text1.txt";' + " filename*=utf-8''text1.txt\r\n" + "\r\n" + "\r\n" + "------aiohttpgraphql--\r\n" ) response = await client.post( @@ -595,7 +587,7 @@ async def test_post_multipart_data(client): ) assert response.status == 200 - assert await response.json() == {"data": {u"writeTest": {u"test": u"Hello World"}}} + assert await response.json() == {"data": {"writeTest": {"test": "Hello World"}}} @pytest.mark.asyncio @@ -674,7 +666,8 @@ async def test_async_schema(app, client): @pytest.mark.asyncio async def test_preflight_request(client): response = await client.options( - "/graphql", headers={"Access-Control-Request-Method": "POST"}, + "/graphql", + headers={"Access-Control-Request-Method": "POST"}, ) assert response.status == 200 @@ -683,7 +676,8 @@ async def test_preflight_request(client): @pytest.mark.asyncio async def test_preflight_incorrect_request(client): response = await client.options( - "/graphql", headers={"Access-Control-Request-Method": "OPTIONS"}, + "/graphql", + headers={"Access-Control-Request-Method": "OPTIONS"}, ) assert response.status == 400 diff --git a/tests/flask/test_graphqlview.py b/tests/flask/test_graphqlview.py index d8d60b0..9b388f9 100644 --- a/tests/flask/test_graphqlview.py +++ b/tests/flask/test_graphqlview.py @@ -97,12 +97,10 @@ def test_reports_validation_errors(app, client): { "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], - "path": None, }, { "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 21}], - "path": None, }, ] } @@ -123,9 +121,8 @@ def test_errors_when_missing_operation_name(app, client): assert response_json(response) == { "errors": [ { - "message": "Must provide operation name if query contains multiple operations.", # noqa: E501 - "locations": None, - "path": None, + "message": "Must provide operation name" + " if query contains multiple operations.", } ] } @@ -145,8 +142,6 @@ def test_errors_when_sending_a_mutation_via_get(app, client): "errors": [ { "message": "Can only perform a mutation operation from a POST request.", - "locations": None, - "path": None, } ] } @@ -169,8 +164,6 @@ def test_errors_when_selecting_a_mutation_within_a_get(app, client): "errors": [ { "message": "Can only perform a mutation operation from a POST request.", - "locations": None, - "path": None, } ] } @@ -272,7 +265,9 @@ def test_supports_post_url_encoded_query_with_string_variables(app, client): def test_supports_post_json_query_with_get_variable_values(app, client): response = client.post( url_string(app, variables=json.dumps({"who": "Dolly"})), - data=json_dump_kwarg(query="query helloWho($who: String){ test(who: $who) }",), + data=json_dump_kwarg( + query="query helloWho($who: String){ test(who: $who) }", + ), content_type="application/json", ) @@ -283,7 +278,11 @@ def test_supports_post_json_query_with_get_variable_values(app, client): def test_post_url_encoded_query_with_get_variable_values(app, client): response = client.post( url_string(app, variables=json.dumps({"who": "Dolly"})), - data=urlencode(dict(query="query helloWho($who: String){ test(who: $who) }",)), + data=urlencode( + dict( + query="query helloWho($who: String){ test(who: $who) }", + ) + ), content_type="application/x-www-form-urlencoded", ) @@ -392,7 +391,6 @@ def test_handles_syntax_errors_caught_by_graphql(app, client): { "locations": [{"column": 1, "line": 1}], "message": "Syntax Error: Unexpected Name 'syntaxerror'.", - "path": None, } ] } @@ -404,7 +402,9 @@ def test_handles_errors_caused_by_a_lack_of_query(app, client): assert response.status_code == 400 assert response_json(response) == { "errors": [ - {"message": "Must provide query string.", "locations": None, "path": None} + { + "message": "Must provide query string.", + } ] } @@ -417,8 +417,6 @@ def test_handles_batch_correctly_if_is_disabled(app, client): "errors": [ { "message": "Batch GraphQL requests are not enabled.", - "locations": None, - "path": None, } ] } @@ -432,7 +430,9 @@ def test_handles_incomplete_json_bodies(app, client): assert response.status_code == 400 assert response_json(response) == { "errors": [ - {"message": "POST body sent invalid JSON.", "locations": None, "path": None} + { + "message": "POST body sent invalid JSON.", + } ] } @@ -446,7 +446,9 @@ def test_handles_plain_post_text(app, client): assert response.status_code == 400 assert response_json(response) == { "errors": [ - {"message": "Must provide query string.", "locations": None, "path": None} + { + "message": "Must provide query string.", + } ] } @@ -462,7 +464,9 @@ def test_handles_poorly_formed_variables(app, client): assert response.status_code == 400 assert response_json(response) == { "errors": [ - {"message": "Variables are invalid JSON.", "locations": None, "path": None} + { + "message": "Variables are invalid JSON.", + } ] } @@ -475,8 +479,6 @@ def test_handles_unsupported_http_methods(app, client): "errors": [ { "message": "GraphQL only supports GET and POST requests.", - "locations": None, - "path": None, } ] } @@ -524,9 +526,7 @@ def test_post_multipart_data(app, client): ) assert response.status_code == 200 - assert response_json(response) == { - "data": {u"writeTest": {u"test": u"Hello World"}} - } + assert response_json(response) == {"data": {"writeTest": {"test": "Hello World"}}} @pytest.mark.parametrize("app", [create_app(batch=True)]) diff --git a/tests/quart/test_graphqlview.py b/tests/quart/test_graphqlview.py index 4a24ace..429b4ef 100644 --- a/tests/quart/test_graphqlview.py +++ b/tests/quart/test_graphqlview.py @@ -35,7 +35,7 @@ async def execute_client( method: str = "GET", data: str = None, headers: Headers = None, - **url_params + **url_params, ) -> Response: if sys.version_info >= (3, 7): test_request_context = app.test_request_context("/", method=method) @@ -126,12 +126,10 @@ async def test_reports_validation_errors(app: Quart, client: QuartClient): { "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], - "path": None, }, { "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 21}], - "path": None, }, ] } @@ -153,9 +151,8 @@ async def test_errors_when_missing_operation_name(app: Quart, client: QuartClien assert response_json(result) == { "errors": [ { - "message": "Must provide operation name if query contains multiple operations.", # noqa: E501 - "locations": None, - "path": None, + "message": "Must provide operation name" + " if query contains multiple operations.", } ] } @@ -176,8 +173,6 @@ async def test_errors_when_sending_a_mutation_via_get(app: Quart, client: QuartC "errors": [ { "message": "Can only perform a mutation operation from a POST request.", - "locations": None, - "path": None, } ] } @@ -203,8 +198,6 @@ async def test_errors_when_selecting_a_mutation_within_a_get( "errors": [ { "message": "Can only perform a mutation operation from a POST request.", - "locations": None, - "path": None, } ] } @@ -342,7 +335,9 @@ async def test_supports_post_json_query_with_get_variable_values( app, client, method="POST", - data=json_dump_kwarg(query="query helloWho($who: String){ test(who: $who) }",), + data=json_dump_kwarg( + query="query helloWho($who: String){ test(who: $who) }", + ), headers=Headers({"Content-Type": "application/json"}), variables=json.dumps({"who": "Dolly"}), ) @@ -360,7 +355,11 @@ async def test_post_url_encoded_query_with_get_variable_values( app, client, method="POST", - data=urlencode(dict(query="query helloWho($who: String){ test(who: $who) }",)), + data=urlencode( + dict( + query="query helloWho($who: String){ test(who: $who) }", + ) + ), headers=Headers({"Content-Type": "application/x-www-form-urlencoded"}), variables=json.dumps({"who": "Dolly"}), ) @@ -463,7 +462,7 @@ async def test_supports_pretty_printing_by_request(app: Quart, client: QuartClie response = await execute_client(app, client, query="{test}", pretty="1") result = await response.get_data(raw=False) - assert result == ("{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}") + assert result == "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" @pytest.mark.asyncio @@ -493,7 +492,6 @@ async def test_handles_syntax_errors_caught_by_graphql(app: Quart, client: Quart { "locations": [{"column": 1, "line": 1}], "message": "Syntax Error: Unexpected Name 'syntaxerror'.", - "path": None, } ] } @@ -508,9 +506,7 @@ async def test_handles_errors_caused_by_a_lack_of_query( assert response.status_code == 400 result = await response.get_data(raw=False) assert response_json(result) == { - "errors": [ - {"message": "Must provide query string.", "locations": None, "path": None} - ] + "errors": [{"message": "Must provide query string."}] } @@ -530,8 +526,6 @@ async def test_handles_batch_correctly_if_is_disabled(app: Quart, client: QuartC "errors": [ { "message": "Batch GraphQL requests are not enabled.", - "locations": None, - "path": None, } ] } @@ -550,9 +544,7 @@ async def test_handles_incomplete_json_bodies(app: Quart, client: QuartClient): assert response.status_code == 400 result = await response.get_data(raw=False) assert response_json(result) == { - "errors": [ - {"message": "POST body sent invalid JSON.", "locations": None, "path": None} - ] + "errors": [{"message": "POST body sent invalid JSON."}] } @@ -569,9 +561,7 @@ async def test_handles_plain_post_text(app: Quart, client: QuartClient): assert response.status_code == 400 result = await response.get_data(raw=False) assert response_json(result) == { - "errors": [ - {"message": "Must provide query string.", "locations": None, "path": None} - ] + "errors": [{"message": "Must provide query string."}] } @@ -586,9 +576,7 @@ async def test_handles_poorly_formed_variables(app: Quart, client: QuartClient): assert response.status_code == 400 result = await response.get_data(raw=False) assert response_json(result) == { - "errors": [ - {"message": "Variables are invalid JSON.", "locations": None, "path": None} - ] + "errors": [{"message": "Variables are invalid JSON."}] } @@ -599,13 +587,7 @@ async def test_handles_unsupported_http_methods(app: Quart, client: QuartClient) result = await response.get_data(raw=False) assert response.headers["Allow"] in ["GET, POST", "HEAD, GET, POST, OPTIONS"] assert response_json(result) == { - "errors": [ - { - "message": "GraphQL only supports GET and POST requests.", - "locations": None, - "path": None, - } - ] + "errors": [{"message": "GraphQL only supports GET and POST requests."}] } diff --git a/tests/sanic/test_graphqlview.py b/tests/sanic/test_graphqlview.py index 740697c..7152150 100644 --- a/tests/sanic/test_graphqlview.py +++ b/tests/sanic/test_graphqlview.py @@ -74,12 +74,10 @@ def test_reports_validation_errors(app): { "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], - "path": None, }, { "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 21}], - "path": None, }, ] } @@ -100,9 +98,8 @@ def test_errors_when_missing_operation_name(app): assert response_json(response) == { "errors": [ { - "locations": None, - "message": "Must provide operation name if query contains multiple operations.", - "path": None, + "message": "Must provide operation name" + " if query contains multiple operations.", } ] } @@ -121,9 +118,7 @@ def test_errors_when_sending_a_mutation_via_get(app): assert response_json(response) == { "errors": [ { - "locations": None, "message": "Can only perform a mutation operation from a POST request.", - "path": None, } ] } @@ -145,9 +140,7 @@ def test_errors_when_selecting_a_mutation_within_a_get(app): assert response_json(response) == { "errors": [ { - "locations": None, "message": "Can only perform a mutation operation from a POST request.", - "path": None, } ] } @@ -260,7 +253,9 @@ def test_supports_post_url_encoded_query_with_string_variables(app): def test_supports_post_json_query_with_get_variable_values(app): _, response = app.client.post( uri=url_string(variables=json.dumps({"who": "Dolly"})), - data=json_dump_kwarg(query="query helloWho($who: String){ test(who: $who) }",), + data=json_dump_kwarg( + query="query helloWho($who: String){ test(who: $who) }", + ), headers={"content-type": "application/json"}, ) @@ -272,7 +267,11 @@ def test_supports_post_json_query_with_get_variable_values(app): def test_post_url_encoded_query_with_get_variable_values(app): _, response = app.client.post( uri=url_string(variables=json.dumps({"who": "Dolly"})), - data=urlencode(dict(query="query helloWho($who: String){ test(who: $who) }",)), + data=urlencode( + dict( + query="query helloWho($who: String){ test(who: $who) }", + ) + ), headers={"content-type": "application/x-www-form-urlencoded"}, ) @@ -387,7 +386,6 @@ def test_handles_syntax_errors_caught_by_graphql(app): { "locations": [{"column": 1, "line": 1}], "message": "Syntax Error: Unexpected Name 'syntaxerror'.", - "path": None, } ] } @@ -399,9 +397,7 @@ def test_handles_errors_caused_by_a_lack_of_query(app): assert response.status == 400 assert response_json(response) == { - "errors": [ - {"locations": None, "message": "Must provide query string.", "path": None} - ] + "errors": [{"message": "Must provide query string."}] } @@ -415,9 +411,7 @@ def test_handles_batch_correctly_if_is_disabled(app): assert response_json(response) == { "errors": [ { - "locations": None, "message": "Batch GraphQL requests are not enabled.", - "path": None, } ] } @@ -431,9 +425,7 @@ def test_handles_incomplete_json_bodies(app): assert response.status == 400 assert response_json(response) == { - "errors": [ - {"locations": None, "message": "POST body sent invalid JSON.", "path": None} - ] + "errors": [{"message": "POST body sent invalid JSON."}] } @@ -446,9 +438,7 @@ def test_handles_plain_post_text(app): ) assert response.status == 400 assert response_json(response) == { - "errors": [ - {"locations": None, "message": "Must provide query string.", "path": None} - ] + "errors": [{"message": "Must provide query string."}] } @@ -461,9 +451,7 @@ def test_handles_poorly_formed_variables(app): ) assert response.status == 400 assert response_json(response) == { - "errors": [ - {"locations": None, "message": "Variables are invalid JSON.", "path": None} - ] + "errors": [{"message": "Variables are invalid JSON."}] } @@ -475,9 +463,7 @@ def test_handles_unsupported_http_methods(app): assert response_json(response) == { "errors": [ { - "locations": None, "message": "GraphQL only supports GET and POST requests.", - "path": None, } ] } @@ -542,9 +528,7 @@ def test_post_multipart_data(app): ) assert response.status == 200 - assert response_json(response) == { - "data": {u"writeTest": {u"test": u"Hello World"}} - } + assert response_json(response) == {"data": {"writeTest": {"test": "Hello World"}}} @pytest.mark.parametrize("app", [create_app(batch=True)]) diff --git a/tests/test_query.py b/tests/test_query.py index c4f6a43..a1352cc 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -40,9 +40,7 @@ def test_validate_schema(): "data": None, "errors": [ { - "locations": None, "message": "Query root type must be provided.", - "path": None, } ], } @@ -109,12 +107,10 @@ def test_reports_validation_errors(): { "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], - "path": None, }, { "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 21}], - "path": None, }, ], } @@ -144,7 +140,6 @@ def enter_field(self, node, *_args): { "message": "Custom validation error.", "locations": [{"line": 1, "column": 3}], - "path": None, } ], } @@ -170,13 +165,10 @@ def test_reports_max_num_of_validation_errors(): { "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], - "path": None, }, { "message": "Too many validation errors, error limit reached." " Validation aborted.", - "locations": None, - "path": None, }, ], } @@ -223,12 +215,10 @@ def test_errors_when_missing_operation_name(): "data": None, "errors": [ { - "locations": None, "message": ( "Must provide operation name" " if query contains multiple operations." ), - "path": None, } ], } @@ -585,8 +575,7 @@ def test_encode_execution_results_batch(): 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}]}]', + '[{"data":{"answer":42}},{"errors":[{"message":"bad"}]}]', 400, ) diff --git a/tests/webob/test_graphqlview.py b/tests/webob/test_graphqlview.py index 456b5f1..e1d783d 100644 --- a/tests/webob/test_graphqlview.py +++ b/tests/webob/test_graphqlview.py @@ -76,12 +76,10 @@ def test_reports_validation_errors(client): { "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], - "path": None, }, { "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 21}], - "path": None, }, ] } @@ -101,9 +99,8 @@ def test_errors_when_missing_operation_name(client): assert response_json(response) == { "errors": [ { - "message": "Must provide operation name if query contains multiple operations.", - "locations": None, - "path": None, + "message": "Must provide operation name" + " if query contains multiple operations.", } ] } @@ -122,8 +119,6 @@ def test_errors_when_sending_a_mutation_via_get(client): "errors": [ { "message": "Can only perform a mutation operation from a POST request.", - "locations": None, - "path": None, } ] } @@ -145,8 +140,6 @@ def test_errors_when_selecting_a_mutation_within_a_get(client): "errors": [ { "message": "Can only perform a mutation operation from a POST request.", - "locations": None, - "path": None, } ] } @@ -247,7 +240,9 @@ def test_supports_post_url_encoded_query_with_string_variables(client): def test_supports_post_json_quey_with_get_variable_values(client): response = client.post( url_string(variables=json.dumps({"who": "Dolly"})), - data=json_dump_kwarg(query="query helloWho($who: String){ test(who: $who) }",), + data=json_dump_kwarg( + query="query helloWho($who: String){ test(who: $who) }", + ), content_type="application/json", ) @@ -258,7 +253,11 @@ def test_supports_post_json_quey_with_get_variable_values(client): def test_post_url_encoded_query_with_get_variable_values(client): response = client.post( url_string(variables=json.dumps({"who": "Dolly"})), - data=urlencode(dict(query="query helloWho($who: String){ test(who: $who) }",)), + data=urlencode( + dict( + query="query helloWho($who: String){ test(who: $who) }", + ) + ), content_type="application/x-www-form-urlencoded", ) @@ -367,7 +366,6 @@ def test_handles_syntax_errors_caught_by_graphql(client): { "message": "Syntax Error: Unexpected Name 'syntaxerror'.", "locations": [{"column": 1, "line": 1}], - "path": None, } ] } @@ -378,9 +376,7 @@ def test_handles_errors_caused_by_a_lack_of_query(client): assert response.status_code == 400 assert response_json(response) == { - "errors": [ - {"message": "Must provide query string.", "locations": None, "path": None} - ] + "errors": [{"message": "Must provide query string."}] } @@ -392,8 +388,6 @@ def test_handles_batch_correctly_if_is_disabled(client): "errors": [ { "message": "Batch GraphQL requests are not enabled.", - "locations": None, - "path": None, } ] } @@ -406,9 +400,7 @@ def test_handles_incomplete_json_bodies(client): assert response.status_code == 400 assert response_json(response) == { - "errors": [ - {"message": "POST body sent invalid JSON.", "locations": None, "path": None} - ] + "errors": [{"message": "POST body sent invalid JSON."}] } @@ -420,9 +412,7 @@ def test_handles_plain_post_text(client): ) assert response.status_code == 400 assert response_json(response) == { - "errors": [ - {"message": "Must provide query string.", "locations": None, "path": None} - ] + "errors": [{"message": "Must provide query string."}] } @@ -434,9 +424,7 @@ def test_handles_poorly_formed_variables(client): ) assert response.status_code == 400 assert response_json(response) == { - "errors": [ - {"message": "Variables are invalid JSON.", "locations": None, "path": None} - ] + "errors": [{"message": "Variables are invalid JSON."}] } @@ -445,13 +433,7 @@ def test_handles_unsupported_http_methods(client): assert response.status_code == 405 assert response.headers["Allow"] in ["GET, POST", "HEAD, GET, POST, OPTIONS"] assert response_json(response) == { - "errors": [ - { - "message": "GraphQL only supports GET and POST requests.", - "locations": None, - "path": None, - } - ] + "errors": [{"message": "GraphQL only supports GET and POST requests."}] } @@ -511,9 +493,7 @@ def test_post_multipart_data(client): ) assert response.status_code == 200 - assert response_json(response) == { - "data": {u"writeTest": {u"test": u"Hello World"}} - } + assert response_json(response) == {"data": {"writeTest": {"test": "Hello World"}}} @pytest.mark.parametrize("settings", [dict(batch=True)]) From bda6a87bb987625908159a80a5563b1a1e7f05e5 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Mon, 17 Jan 2022 13:20:10 +0100 Subject: [PATCH 04/20] Update dependencies --- setup.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 6bb761e..18294bf 100644 --- a/setup.py +++ b/setup.py @@ -4,27 +4,27 @@ install_requires = ["graphql-core>=3.2,<3.3", "typing-extensions>=4,<5"] tests_requires = [ - "pytest>=5.4,<5.5", - "pytest-asyncio>=0.11.0", - "pytest-cov>=2.8,<3", - "aiohttp>=3.5.0,<4", - "Jinja2>=2.10.1,<3", + "pytest>=6.2,<6.3", + "pytest-asyncio>=0.17,<1", + "pytest-cov>=3,<4", + "aiohttp>=3.8,<4", + "Jinja2>=2.11,<3", ] dev_requires = [ "flake8>=4,<5", "isort>=5,<6", "black>=19.10b0", - "mypy>=0.761,<0.770", - "check-manifest>=0.40,<1", + "mypy>=0.931,<1", + "check-manifest>=0.47,<1", ] + tests_requires install_flask_requires = [ - "flask>=0.7.0<1", + "flask>=1,<2", ] install_sanic_requires = [ - "sanic>=20.3.0,<21", + "sanic>=21,<22", ] install_webob_requires = [ @@ -32,7 +32,7 @@ ] install_aiohttp_requires = [ - "aiohttp>=3.5.0,<4", + "aiohttp>=3.8,<4", ] install_quart_requires = ["quart>=0.6.15,<1"] From ec4ed15046c7b133907c9250a8101b01fe94eaaf Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Mon, 17 Jan 2022 14:13:52 +0100 Subject: [PATCH 05/20] Support Python 3.10 Also restrict web frameworks to supported versions --- .github/workflows/deploy.yml | 6 +++--- .github/workflows/lint.yml | 6 +++--- .github/workflows/tests.yml | 10 ++++++---- graphql_server/__init__.py | 14 ++++++++++++-- graphql_server/aiohttp/graphqlview.py | 4 +++- graphql_server/flask/graphqlview.py | 2 +- graphql_server/sanic/graphqlview.py | 4 ++-- graphql_server/webob/graphqlview.py | 2 +- setup.py | 5 +++-- tests/aiohttp/schema.py | 5 ++++- tests/aiohttp/test_graphiqlview.py | 17 ++++++++++++----- tests/quart/test_graphiqlview.py | 7 +------ tests/quart/test_graphqlview.py | 8 +------- tests/sanic/app.py | 1 - tox.ini | 17 +++++++++-------- 15 files changed, 61 insertions(+), 47 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a580073..6a34bba 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,10 +11,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Build wheel and source tarball run: | pip install wheel @@ -23,4 +23,4 @@ jobs: uses: pypa/gh-action-pypi-publish@v1.1.0 with: user: __token__ - password: ${{ secrets.pypi_password }} \ No newline at end of file + password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 252a382..90ba2a1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,10 +8,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -19,4 +19,4 @@ jobs: - name: Run lint and static type checks run: tox env: - TOXENV: flake8,black,import-order,mypy,manifest \ No newline at end of file + TOXENV: flake8,black,import-order,mypy,manifest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4110dae..31616ec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.6", "3.7", "3.8", "3.9"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] os: [ubuntu-latest, windows-latest] exclude: - os: windows-latest @@ -16,7 +16,9 @@ jobs: - os: windows-latest python-version: "3.7" - os: windows-latest - python-version: "3.9" + python-version: "3.8" + - os: windows-latest + python-version: "3.10" steps: - uses: actions/checkout@v2 @@ -38,10 +40,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install test dependencies run: | python -m pip install --upgrade pip diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index 5ae4acd..ee54cdb 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -9,7 +9,17 @@ import json from collections import namedtuple from collections.abc import MutableMapping -from typing import Any, Callable, Collection, Dict, List, Optional, Type, Union +from typing import ( + Any, + Callable, + Collection, + Dict, + List, + Optional, + Type, + Union, + cast, +) from graphql.error import GraphQLError from graphql.execution import ExecutionResult, execute @@ -56,7 +66,7 @@ def format_error_default(error: GraphQLError) -> Dict: """The default function for converting GraphQLError to a dictionary.""" - return error.formatted + return cast(Dict, error.formatted) def run_http_query( diff --git a/graphql_server/aiohttp/graphqlview.py b/graphql_server/aiohttp/graphqlview.py index deb6522..d98becd 100644 --- a/graphql_server/aiohttp/graphqlview.py +++ b/graphql_server/aiohttp/graphqlview.py @@ -201,7 +201,9 @@ async def __call__(self, request): return web.Response(text=source, content_type="text/html") return web.Response( - text=result, status=status_code, content_type="application/json", + text=result, + status=status_code, + content_type="application/json", ) except HttpQueryError as err: diff --git a/graphql_server/flask/graphqlview.py b/graphql_server/flask/graphqlview.py index 2a9e451..063a67a 100644 --- a/graphql_server/flask/graphqlview.py +++ b/graphql_server/flask/graphqlview.py @@ -5,9 +5,9 @@ from flask import Response, render_template_string, request from flask.views import View +from graphql import specified_rules from graphql.error import GraphQLError from graphql.type.schema import GraphQLSchema -from graphql import specified_rules from graphql_server import ( GraphQLParams, diff --git a/graphql_server/sanic/graphqlview.py b/graphql_server/sanic/graphqlview.py index c7a3b75..569db53 100644 --- a/graphql_server/sanic/graphqlview.py +++ b/graphql_server/sanic/graphqlview.py @@ -212,8 +212,8 @@ def request_wants_html(request): return "text/html" in accept or "*/*" in accept def process_preflight(self, request): - """ Preflight request support for apollo-client - https://www.w3.org/TR/cors/#resource-preflight-requests """ + """Preflight request support for apollo-client + https://www.w3.org/TR/cors/#resource-preflight-requests""" origin = request.headers.get("Origin", "") method = request.headers.get("Access-Control-Request-Method", "").upper() diff --git a/graphql_server/webob/graphqlview.py b/graphql_server/webob/graphqlview.py index 0aa08c6..36725f3 100644 --- a/graphql_server/webob/graphqlview.py +++ b/graphql_server/webob/graphqlview.py @@ -3,9 +3,9 @@ from functools import partial from typing import List +from graphql import specified_rules from graphql.error import GraphQLError from graphql.type.schema import GraphQLSchema -from graphql import specified_rules from webob import Response from graphql_server import ( diff --git a/setup.py b/setup.py index 18294bf..bb98728 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ ] install_sanic_requires = [ - "sanic>=21,<22", + "sanic>=20.3,<21", ] install_webob_requires = [ @@ -35,7 +35,7 @@ "aiohttp>=3.8,<4", ] -install_quart_requires = ["quart>=0.6.15,<1"] +install_quart_requires = ["quart>=0.6.15,<0.15"] install_all_requires = ( install_requires @@ -71,6 +71,7 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "License :: OSI Approved :: MIT License", ], keywords="api graphql protocol rest", diff --git a/tests/aiohttp/schema.py b/tests/aiohttp/schema.py index 7673180..54e0d10 100644 --- a/tests/aiohttp/schema.py +++ b/tests/aiohttp/schema.py @@ -18,7 +18,10 @@ def resolve_raises(*_): QueryRootType = GraphQLObjectType( name="QueryRoot", fields={ - "thrower": GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_raises,), + "thrower": GraphQLField( + GraphQLNonNull(GraphQLString), + resolve=resolve_raises, + ), "request": GraphQLField( GraphQLNonNull(GraphQLString), resolve=lambda obj, info, *args: info.context["request"].query.get("q"), diff --git a/tests/aiohttp/test_graphiqlview.py b/tests/aiohttp/test_graphiqlview.py index 111b603..4e5bd32 100644 --- a/tests/aiohttp/test_graphiqlview.py +++ b/tests/aiohttp/test_graphiqlview.py @@ -52,7 +52,8 @@ async def test_graphiql_is_enabled(app, client): @pytest.mark.parametrize("app", [create_app(graphiql=True)]) async def test_graphiql_simple_renderer(app, client, pretty_response): response = await client.get( - url_string(query="{test}"), headers={"Accept": "text/html"}, + url_string(query="{test}"), + headers={"Accept": "text/html"}, ) assert response.status == 200 assert pretty_response in await response.text() @@ -65,7 +66,8 @@ class TestJinjaEnv: ) async def test_graphiql_jinja_renderer_async(self, app, client, pretty_response): response = await client.get( - url_string(query="{test}"), headers={"Accept": "text/html"}, + url_string(query="{test}"), + headers={"Accept": "text/html"}, ) assert response.status == 200 assert pretty_response in await response.text() @@ -73,7 +75,10 @@ async def test_graphiql_jinja_renderer_async(self, app, client, pretty_response) @pytest.mark.asyncio async def test_graphiql_html_is_not_accepted(client): - response = await client.get("/graphql", headers={"Accept": "application/json"},) + response = await client.get( + "/graphql", + headers={"Accept": "application/json"}, + ) assert response.status == 400 @@ -107,7 +112,8 @@ async def test_graphiql_get_subscriptions(app, client): ) async def test_graphiql_enabled_async_schema(app, client): response = await client.get( - url_string(query="{a,b,c}"), headers={"Accept": "text/html"}, + url_string(query="{a,b,c}"), + headers={"Accept": "text/html"}, ) expected_response = ( @@ -133,7 +139,8 @@ async def test_graphiql_enabled_async_schema(app, client): ) async def test_graphiql_enabled_sync_schema(app, client): response = await client.get( - url_string(query="{a,b}"), headers={"Accept": "text/html"}, + url_string(query="{a,b}"), + headers={"Accept": "text/html"}, ) expected_response = ( diff --git a/tests/quart/test_graphiqlview.py b/tests/quart/test_graphiqlview.py index 12b001f..1d8d7e3 100644 --- a/tests/quart/test_graphiqlview.py +++ b/tests/quart/test_graphiqlview.py @@ -1,5 +1,3 @@ -import sys - import pytest from quart import Quart, Response, url_for from quart.testing import QuartClient @@ -32,10 +30,7 @@ async def execute_client( headers: Headers = None, **extra_params ) -> Response: - if sys.version_info >= (3, 7): - test_request_context = app.test_request_context("/", method=method) - else: - test_request_context = app.test_request_context(method, "/") + test_request_context = app.test_request_context(path="/", method=method) async with test_request_context: string = url_for("graphql", **extra_params) return await client.get(string, headers=headers) diff --git a/tests/quart/test_graphqlview.py b/tests/quart/test_graphqlview.py index 429b4ef..79d1f73 100644 --- a/tests/quart/test_graphqlview.py +++ b/tests/quart/test_graphqlview.py @@ -1,7 +1,4 @@ import json -import sys - -# from io import StringIO from urllib.parse import urlencode import pytest @@ -37,10 +34,7 @@ async def execute_client( headers: Headers = None, **url_params, ) -> Response: - if sys.version_info >= (3, 7): - test_request_context = app.test_request_context("/", method=method) - else: - test_request_context = app.test_request_context(method, "/") + test_request_context = app.test_request_context(path="/", method=method) async with test_request_context: string = url_for("graphql") diff --git a/tests/sanic/app.py b/tests/sanic/app.py index 6966b1e..84269cc 100644 --- a/tests/sanic/app.py +++ b/tests/sanic/app.py @@ -7,7 +7,6 @@ from .schema import Schema - Sanic.test_mode = True diff --git a/tox.ini b/tox.ini index e374ee0..047d8a6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] -envlist = +envlist = black,flake8,import-order,mypy,manifest, - py{36,37,38,39} + py{36,37,38,39,310} ; requires = tox-conda [gh-actions] @@ -10,6 +10,7 @@ python = 3.7: py37 3.8: py38 3.9: py39 + 3.10: py310 [testenv] conda_channels = conda-forge @@ -26,31 +27,31 @@ commands = py{38}: pytest tests --cov-report=term-missing --cov=graphql_server {posargs} [testenv:black] -basepython = python3.8 +basepython = python3.9 deps = -e.[dev] commands = black --check graphql_server tests [testenv:flake8] -basepython = python3.8 +basepython = python3.9 deps = -e.[dev] commands = flake8 setup.py graphql_server tests [testenv:import-order] -basepython = python3.8 +basepython = python3.9 deps = -e.[dev] commands = - isort -rc graphql_server/ tests/ + isort graphql_server/ tests/ [testenv:mypy] -basepython = python3.8 +basepython = python3.9 deps = -e.[dev] commands = mypy graphql_server tests --ignore-missing-imports [testenv:manifest] -basepython = python3.8 +basepython = python3.9 deps = -e.[dev] commands = check-manifest -v From eec3d3331413c0b3da4a4dca5e77dc2df7c74090 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Mon, 17 Jan 2022 14:24:07 +0100 Subject: [PATCH 06/20] Make teste work with Python 3.6 again Note that pytest-asyncio 0.17 is not supported for Python 3.6. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bb98728..e2dfcaf 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ tests_requires = [ "pytest>=6.2,<6.3", - "pytest-asyncio>=0.17,<1", + "pytest-asyncio>=0.16,<1", "pytest-cov>=3,<4", "aiohttp>=3.8,<4", "Jinja2>=2.11,<3", From 8dec731311a653f6a3ebd5b91c51ad0ff9bb4bab Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Mon, 17 Jan 2022 14:28:52 +0100 Subject: [PATCH 07/20] Release a new beta version --- graphql_server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql_server/version.py b/graphql_server/version.py index 2ee7c44..d159828 100644 --- a/graphql_server/version.py +++ b/graphql_server/version.py @@ -4,7 +4,7 @@ __all__ = ["version", "version_info"] -version = "3.0.0b4" +version = "3.0.0b5" _re_version = re.compile(r"(\d+)\.(\d+)\.(\d+)(\D*)(\d*)") From 184ba72578101ad7b11a2008e544d5432f627146 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Mon, 26 Dec 2022 02:59:20 +0800 Subject: [PATCH 08/20] chore: update dependencies (#99) * Update dependencies * Relax flask dependency to allow flask 2 * Fixes for quart >=0.15 Fix quart.request.get_data signature QuartClient -> TestClientProtocol * Lint * Fix aiohttp tests * Update sanic to v22.6 * Make sanic v22.9 work * Fix deprecation warnings DeprecationWarning: Use 'content=<...>' to upload raw bytes/text content. * Update graphiql to 1.4.7 for security reason "All versions of graphiql < 1.4.7 are vulnerable to an XSS attack." https://github.com/graphql/graphiql/blob/ab2b52f06213bd9bf90c905c1b460b6939f3d856/docs/security/2021-introspection-schema-xss.md * Fix webob graphiql check Was working by accident before * Fix quart PytestCollectionWarning cannot collect test class 'TestClientProtocol' because it has a __init__ constructor * Make Jinja2 optional * Add python 3.11 and remove 3.6 * Tweak quart for python 3.7 to 3.11 * Fix test for python 3.11 Co-authored-by: Giovanni Campagna Co-authored-by: Choongkyu Kim --- .github/workflows/deploy.yml | 8 +- .github/workflows/lint.yml | 8 +- .github/workflows/tests.yml | 20 +-- graphql_server/__init__.py | 12 +- graphql_server/quart/graphqlview.py | 27 ++-- graphql_server/render_graphiql.py | 17 ++- graphql_server/sanic/graphqlview.py | 4 +- graphql_server/webob/graphqlview.py | 8 +- setup.cfg | 1 + setup.py | 27 ++-- tests/aiohttp/test_graphiqlview.py | 3 +- tests/aiohttp/test_graphqlview.py | 3 +- tests/quart/conftest.py | 3 + tests/quart/test_graphiqlview.py | 24 ++-- tests/quart/test_graphqlview.py | 183 ++++++++++++++++------------ tests/sanic/app.py | 9 +- tests/sanic/test_graphiqlview.py | 14 +-- tests/sanic/test_graphqlview.py | 127 ++++++++++--------- tests/test_asyncio.py | 13 +- tox.ini | 18 +-- 20 files changed, 271 insertions(+), 258 deletions(-) create mode 100644 tests/quart/conftest.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6a34bba..29bb7d1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,11 +10,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.10" - name: Build wheel and source tarball run: | pip install wheel diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 90ba2a1..454ab1b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 31616ec..7e58bb5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,22 +8,22 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] os: [ubuntu-latest, windows-latest] exclude: - - os: windows-latest - python-version: "3.6" - os: windows-latest python-version: "3.7" - os: windows-latest python-version: "3.8" - os: windows-latest - python-version: "3.10" + python-version: "3.9" + - os: windows-latest + python-version: "3.11" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -39,11 +39,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.10" - name: Install test dependencies run: | python -m pip install --upgrade pip diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index ee54cdb..9a58a9f 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -9,17 +9,7 @@ import json from collections import namedtuple from collections.abc import MutableMapping -from typing import ( - Any, - Callable, - Collection, - Dict, - List, - Optional, - Type, - Union, - cast, -) +from typing import Any, Callable, Collection, Dict, List, Optional, Type, Union, cast from graphql.error import GraphQLError from graphql.execution import ExecutionResult, execute diff --git a/graphql_server/quart/graphqlview.py b/graphql_server/quart/graphqlview.py index ff737ec..107cfdc 100644 --- a/graphql_server/quart/graphqlview.py +++ b/graphql_server/quart/graphqlview.py @@ -1,5 +1,4 @@ import copy -import sys from collections.abc import MutableMapping from functools import partial from typing import List @@ -165,11 +164,11 @@ async def parse_body(): # information provided by content_type content_type = request.mimetype if content_type == "application/graphql": - refined_data = await request.get_data(raw=False) + refined_data = await request.get_data(as_text=True) return {"query": refined_data} elif content_type == "application/json": - refined_data = await request.get_data(raw=False) + refined_data = await request.get_data(as_text=True) return load_json_body(refined_data) elif content_type == "application/x-www-form-urlencoded": @@ -191,20 +190,8 @@ def should_display_graphiql(self): def request_wants_html(): best = request.accept_mimetypes.best_match(["application/json", "text/html"]) - # Needed as this was introduced at Quart 0.8.0: https://gitlab.com/pgjones/quart/-/issues/189 - def _quality(accept, key: str) -> float: - for option in accept.options: - if accept._values_match(key, option.value): - return option.quality - return 0.0 - - if sys.version_info >= (3, 7): - return ( - best == "text/html" - and request.accept_mimetypes[best] - > request.accept_mimetypes["application/json"] - ) - else: - return best == "text/html" and _quality( - request.accept_mimetypes, best - ) > _quality(request.accept_mimetypes, "application/json") + return ( + best == "text/html" + and request.accept_mimetypes[best] + > request.accept_mimetypes["application/json"] + ) diff --git a/graphql_server/render_graphiql.py b/graphql_server/render_graphiql.py index c942300..498f53b 100644 --- a/graphql_server/render_graphiql.py +++ b/graphql_server/render_graphiql.py @@ -1,4 +1,4 @@ -"""Based on (express-graphql)[https://github.com/graphql/express-graphql/blob/master/src/renderGraphiQL.js] and +"""Based on (express-graphql)[https://github.com/graphql/express-graphql/blob/main/src/renderGraphiQL.ts] and (subscriptions-transport-ws)[https://github.com/apollographql/subscriptions-transport-ws]""" import json import re @@ -7,7 +7,7 @@ from jinja2 import Environment from typing_extensions import TypedDict -GRAPHIQL_VERSION = "1.0.3" +GRAPHIQL_VERSION = "1.4.7" GRAPHIQL_TEMPLATE = """