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 = """

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy