From 6a5b28d09f6ea151c0eebba07bfb328230ae9767 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Thu, 30 Mar 2023 19:32:36 +0100 Subject: [PATCH 01/35] Resolve DjangoObjectType getNode when in an async context --- examples/cookbook/cookbook/urls.py | 7 +++---- graphene_django/types.py | 8 ++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/cookbook/cookbook/urls.py b/examples/cookbook/cookbook/urls.py index 6f8a3021c..e9e69cd25 100644 --- a/examples/cookbook/cookbook/urls.py +++ b/examples/cookbook/cookbook/urls.py @@ -1,10 +1,9 @@ -from django.conf.urls import url +from django.urls import re_path from django.contrib import admin from graphene_django.views import GraphQLView - urlpatterns = [ - url(r"^admin/", admin.site.urls), - url(r"^graphql$", GraphQLView.as_view(graphiql=True)), + re_path(r"^admin/", admin.site.urls), + re_path(r"^graphql$", GraphQLView.as_view(graphiql=True)), ] diff --git a/graphene_django/types.py b/graphene_django/types.py index a6e54af41..068d2680d 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -288,6 +288,14 @@ def get_queryset(cls, queryset, info): def get_node(cls, info, id): queryset = cls.get_queryset(cls._meta.model.objects, info) try: + try: + import asyncio + asyncio.get_running_loop() + except RuntimeError: + pass + else: + return queryset.aget(pk=id) + return queryset.get(pk=id) except cls._meta.model.DoesNotExist: return None From 74998afb69ad75d358c5853d3504d69e1b54618c Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Thu, 30 Mar 2023 19:49:27 +0100 Subject: [PATCH 02/35] Support Django Connection resolving in an async context --- graphene_django/fields.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 0fe123deb..80107ea85 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -9,7 +9,8 @@ offset_to_cursor, ) -from promise import Promise +from asgiref.sync import sync_to_async +from asyncio import get_running_loop from graphene import Int, NonNull from graphene.relay import ConnectionField @@ -228,20 +229,43 @@ def connection_resolver( # eventually leads to DjangoObjectType's get_queryset (accepts queryset) # or a resolve_foo (does not accept queryset) + iterable = resolver(root, info, **args) + if info.is_awaitable(iterable): + async def await_result(): + queryset_or_list = await iterable + if queryset_or_list is None: + queryset_or_list = default_manager + + if is_async(queryset_resolver): + + resolved = await sync_to_async(queryset_resolver)(connection, resolved, info, args) + + # TODO: create an async_resolve_connection which uses the new Django queryset async functions + async_resolve_connection = sync_to_async(cls.resolve_connection) + + if is_awaitable(resolved): + return async_resolve_connection(connection, args, await resolved, max_limit=max_limit) + + return async_resolve_connection(connection, args, resolved, max_limit=max_limit) + + return await_result() + if iterable is None: iterable = default_manager # thus the iterable gets refiltered by resolve_queryset # but iterable might be promise iterable = queryset_resolver(connection, iterable, info, args) - on_resolve = partial( - cls.resolve_connection, connection, args, max_limit=max_limit - ) - if Promise.is_thenable(iterable): - return Promise.resolve(iterable).then(on_resolve) + try: + get_running_loop() + except RuntimeError: + pass + else: + return sync_to_async(cls.resolve_connection)(connection, args, iterable, max_limit=max_limit) + - return on_resolve(iterable) + return cls.resolve_connection(connection, args, iterable, max_limit=max_limit) def wrap_resolve(self, parent_resolver): return partial( From f04f0d33ff288a4345a57cf490074fcea3f7465b Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Thu, 30 Mar 2023 19:51:40 +0100 Subject: [PATCH 03/35] Support foriegn key connections running async --- graphene_django/converter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 375d68312..dbd7cb8db 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -1,5 +1,6 @@ from collections import OrderedDict from functools import singledispatch, wraps +from asyncio import get_running_loop from django.db import models from django.utils.encoding import force_str From e78fb86ce66941cc5e0f71046baddd97ae196897 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Thu, 30 Mar 2023 20:27:15 +0100 Subject: [PATCH 04/35] handle regualr django lists --- graphene_django/fields.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 80107ea85..68db6a017 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -61,6 +61,13 @@ def list_resolver( # Pass queryset to the DjangoObjectType get_queryset method queryset = maybe_queryset(django_object_type.get_queryset(queryset, info)) + try: + get_running_loop() + except RuntimeError: + pass + else: + return queryset.aiterator() + return queryset def wrap_resolve(self, parent_resolver): @@ -231,26 +238,7 @@ def connection_resolver( # or a resolve_foo (does not accept queryset) iterable = resolver(root, info, **args) - if info.is_awaitable(iterable): - async def await_result(): - queryset_or_list = await iterable - if queryset_or_list is None: - queryset_or_list = default_manager - - if is_async(queryset_resolver): - - resolved = await sync_to_async(queryset_resolver)(connection, resolved, info, args) - - # TODO: create an async_resolve_connection which uses the new Django queryset async functions - async_resolve_connection = sync_to_async(cls.resolve_connection) - - if is_awaitable(resolved): - return async_resolve_connection(connection, args, await resolved, max_limit=max_limit) - - return async_resolve_connection(connection, args, resolved, max_limit=max_limit) - - return await_result() - + if iterable is None: iterable = default_manager # thus the iterable gets refiltered by resolve_queryset From 28846f9ac954c91422d51f85798ecc703a6eb5b9 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Thu, 30 Mar 2023 21:01:42 +0100 Subject: [PATCH 05/35] drop in an async view --- graphene_django/views.py | 354 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 354 insertions(+) diff --git a/graphene_django/views.py b/graphene_django/views.py index b29aeede7..1ea6db8ae 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -2,12 +2,15 @@ import json import re +from asyncio import gather, coroutines + from django.db import connection, transaction from django.http import HttpResponse, HttpResponseNotAllowed from django.http.response import HttpResponseBadRequest from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.decorators.csrf import ensure_csrf_cookie +from django.utils.decorators import classonlymethod from django.views.generic import View from graphql import OperationType, get_operation_ast, parse, validate from graphql.error import GraphQLError @@ -396,3 +399,354 @@ def get_content_type(request): meta = request.META content_type = meta.get("CONTENT_TYPE", meta.get("HTTP_CONTENT_TYPE", "")) return content_type.split(";", 1)[0].lower() + + +class AsyncGraphQLView(GraphQLView): + graphiql_template = "graphene/graphiql.html" + + # Polyfill for window.fetch. + whatwg_fetch_version = "3.6.2" + whatwg_fetch_sri = "sha256-+pQdxwAcHJdQ3e/9S4RK6g8ZkwdMgFQuHvLuN5uyk5c=" + + # React and ReactDOM. + react_version = "17.0.2" + react_sri = "sha256-Ipu/TQ50iCCVZBUsZyNJfxrDk0E2yhaEIz0vqI+kFG8=" + react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=" + + # The GraphiQL React app. + graphiql_version = "1.4.7" # "1.0.3" + graphiql_sri = "sha256-cpZ8w9D/i6XdEbY/Eu7yAXeYzReVw0mxYd7OU3gUcsc=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk=" + graphiql_css_sri = "sha256-HADQowUuFum02+Ckkv5Yu5ygRoLllHZqg0TFZXY7NHI=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E=" + + # The websocket transport library for subscriptions. + subscriptions_transport_ws_version = "0.9.18" + subscriptions_transport_ws_sri = ( + "sha256-i0hAXd4PdJ/cHX3/8tIy/Q/qKiWr5WSTxMFuL9tACkw=" + ) + + schema = None + graphiql = False + middleware = None + root_value = None + pretty = False + batch = False + subscription_path = None + execution_context_class = None + + def __init__( + self, + schema=None, + middleware=None, + root_value=None, + graphiql=False, + pretty=False, + batch=False, + subscription_path=None, + execution_context_class=None, + ): + if not schema: + schema = graphene_settings.SCHEMA + + if middleware is None: + middleware = graphene_settings.MIDDLEWARE + + self.schema = self.schema or schema + if middleware is not None: + if isinstance(middleware, MiddlewareManager): + self.middleware = middleware + else: + self.middleware = list(instantiate_middleware(middleware)) + self.root_value = root_value + self.pretty = self.pretty or pretty + self.graphiql = self.graphiql or graphiql + self.batch = self.batch or batch + self.execution_context_class = execution_context_class + if subscription_path is None: + self.subscription_path = graphene_settings.SUBSCRIPTION_PATH + + assert isinstance( + self.schema, Schema + ), "A Schema is required to be provided to GraphQLView." + assert not all((graphiql, batch)), "Use either graphiql or batch processing" + + # noinspection PyUnusedLocal + def get_root_value(self, request): + return self.root_value + + def get_middleware(self, request): + return self.middleware + + def get_context(self, request): + return request + + @classonlymethod + def as_view(cls, **initkwargs): + view = super().as_view(**initkwargs) + view._is_coroutine = coroutines._is_coroutine + return view + + @method_decorator(ensure_csrf_cookie) + async def dispatch(self, request, *args, **kwargs): + try: + if request.method.lower() not in ("get", "post"): + raise HttpError( + HttpResponseNotAllowed( + ["GET", "POST"], "GraphQL only supports GET and POST requests." + ) + ) + + data = self.parse_body(request) + show_graphiql = self.graphiql and self.can_display_graphiql(request, data) + + if show_graphiql: + return self.render_graphiql( + request, + # Dependency parameters. + whatwg_fetch_version=self.whatwg_fetch_version, + whatwg_fetch_sri=self.whatwg_fetch_sri, + react_version=self.react_version, + react_sri=self.react_sri, + react_dom_sri=self.react_dom_sri, + graphiql_version=self.graphiql_version, + graphiql_sri=self.graphiql_sri, + graphiql_css_sri=self.graphiql_css_sri, + subscriptions_transport_ws_version=self.subscriptions_transport_ws_version, + subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri, + # The SUBSCRIPTION_PATH setting. + subscription_path=self.subscription_path, + # GraphiQL headers tab, + graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED, + graphiql_should_persist_headers=graphene_settings.GRAPHIQL_SHOULD_PERSIST_HEADERS, + ) + + if self.batch: + responses = await gather(*[self.get_response(request, entry) for entry in data]) + result = "[{}]".format( + ",".join([response[0] for response in responses]) + ) + status_code = ( + responses + and max(responses, key=lambda response: response[1])[1] + or 200 + ) + else: + result, status_code = await self.get_response(request, data, show_graphiql) + + return HttpResponse( + status=status_code, content=result, content_type="application/json" + ) + + except HttpError as e: + response = e.response + response["Content-Type"] = "application/json" + response.content = self.json_encode( + request, {"errors": [self.format_error(e)]} + ) + return response + + async def get_response(self, request, data, show_graphiql=False): + query, variables, operation_name, id = self.get_graphql_params(request, data) + + execution_result = await self.execute_graphql_request( + request, data, query, variables, operation_name, show_graphiql + ) + + if getattr(request, MUTATION_ERRORS_FLAG, False) is True: + set_rollback() + + status_code = 200 + if execution_result: + response = {} + + if execution_result.errors: + set_rollback() + response["errors"] = [ + self.format_error(e) for e in execution_result.errors + ] + + if execution_result.errors and any( + not getattr(e, "path", None) for e in execution_result.errors + ): + status_code = 400 + else: + response["data"] = execution_result.data + + if self.batch: + response["id"] = id + response["status"] = status_code + + result = self.json_encode(request, response, pretty=show_graphiql) + else: + result = None + + return result, status_code + + def render_graphiql(self, request, **data): + return render(request, self.graphiql_template, data) + + def json_encode(self, request, d, pretty=False): + if not (self.pretty or pretty) and not request.GET.get("pretty"): + return json.dumps(d, separators=(",", ":")) + + return json.dumps(d, sort_keys=True, indent=2, separators=(",", ": ")) + + def parse_body(self, request): + content_type = self.get_content_type(request) + + if content_type == "application/graphql": + return {"query": request.body.decode()} + + elif content_type == "application/json": + # noinspection PyBroadException + try: + body = request.body.decode("utf-8") + except Exception as e: + raise HttpError(HttpResponseBadRequest(str(e))) + + try: + request_json = json.loads(body) + if self.batch: + assert isinstance(request_json, list), ( + "Batch requests should receive a list, but received {}." + ).format(repr(request_json)) + assert ( + len(request_json) > 0 + ), "Received an empty list in the batch request." + else: + assert isinstance( + request_json, dict + ), "The received data is not a valid JSON query." + return request_json + except AssertionError as e: + raise HttpError(HttpResponseBadRequest(str(e))) + except (TypeError, ValueError): + raise HttpError(HttpResponseBadRequest("POST body sent invalid JSON.")) + + elif content_type in [ + "application/x-www-form-urlencoded", + "multipart/form-data", + ]: + return request.POST + + return {} + + async def execute_graphql_request( + self, request, data, query, variables, operation_name, show_graphiql=False + ): + if not query: + if show_graphiql: + return None + raise HttpError(HttpResponseBadRequest("Must provide query string.")) + + try: + document = parse(query) + except Exception as e: + return ExecutionResult(errors=[e]) + + if request.method.lower() == "get": + operation_ast = get_operation_ast(document, operation_name) + if operation_ast and operation_ast.operation != OperationType.QUERY: + if show_graphiql: + return None + + raise HttpError( + HttpResponseNotAllowed( + ["POST"], + "Can only perform a {} operation from a POST request.".format( + operation_ast.operation.value + ), + ) + ) + + validation_errors = validate(self.schema.graphql_schema, document) + if validation_errors: + return ExecutionResult(data=None, errors=validation_errors) + + try: + extra_options = {} + if self.execution_context_class: + extra_options["execution_context_class"] = self.execution_context_class + + options = { + "source": query, + "root_value": self.get_root_value(request), + "variable_values": variables, + "operation_name": operation_name, + "context_value": self.get_context(request), + "middleware": self.get_middleware(request), + } + options.update(extra_options) + + operation_ast = get_operation_ast(document, operation_name) + if ( + operation_ast + and operation_ast.operation == OperationType.MUTATION + and ( + graphene_settings.ATOMIC_MUTATIONS is True + or connection.settings_dict.get("ATOMIC_MUTATIONS", False) is True + ) + ): + with transaction.atomic(): + result = await self.schema.execute_async(**options) + if getattr(request, MUTATION_ERRORS_FLAG, False) is True: + transaction.set_rollback(True) + return result + + return await self.schema.execute_async(**options) + except Exception as e: + return ExecutionResult(errors=[e]) + + @classmethod + def can_display_graphiql(cls, request, data): + raw = "raw" in request.GET or "raw" in data + return not raw and cls.request_wants_html(request) + + @classmethod + def request_wants_html(cls, request): + accepted = get_accepted_content_types(request) + accepted_length = len(accepted) + # the list will be ordered in preferred first - so we have to make + # sure the most preferred gets the highest number + html_priority = ( + accepted_length - accepted.index("text/html") + if "text/html" in accepted + else 0 + ) + json_priority = ( + accepted_length - accepted.index("application/json") + if "application/json" in accepted + else 0 + ) + + return html_priority > json_priority + + @staticmethod + def get_graphql_params(request, data): + query = request.GET.get("query") or data.get("query") + variables = request.GET.get("variables") or data.get("variables") + id = request.GET.get("id") or data.get("id") + + if variables and isinstance(variables, str): + try: + variables = json.loads(variables) + except Exception: + raise HttpError(HttpResponseBadRequest("Variables are invalid JSON.")) + + operation_name = request.GET.get("operationName") or data.get("operationName") + if operation_name == "null": + operation_name = None + + return query, variables, operation_name, id + + @staticmethod + def format_error(error): + if isinstance(error, GraphQLError): + return error.formatted + + return {"message": str(error)} + + @staticmethod + def get_content_type(request): + meta = request.META + content_type = meta.get("CONTENT_TYPE", meta.get("HTTP_CONTENT_TYPE", "")) + return content_type.split(";", 1)[0].lower() From 7ddaf9f5e683d31a5e9907c80e57e83e4c332bfd Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Fri, 31 Mar 2023 11:28:57 -0700 Subject: [PATCH 06/35] Handle coroutine results from resolvers in connections and filter connections --- graphene_django/fields.py | 14 ++++++++++++++ graphene_django/filter/fields.py | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 68db6a017..c8899de2f 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -239,6 +239,20 @@ def connection_resolver( iterable = resolver(root, info, **args) + if info.is_awaitable(iterable): + async def resolve_connection_async(): + iterable = await iterable + if iterable is None: + iterable = default_manager + ## This could also be async + iterable = queryset_resolver(connection, iterable, info, args) + + if info.is_awaitable(iterable): + iterable = await iterable + + return await sync_to_async(cls.resolve_connection)(connection, args, iterable, max_limit=max_limit) + return resolve_connection_async() + if iterable is None: iterable = default_manager # thus the iterable gets refiltered by resolve_queryset diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index cdb8f850f..a14146058 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -7,6 +7,8 @@ from graphene.types.argument import to_arguments from graphene.utils.str_converters import to_snake_case +from asgiref.sync import sync_to_async + from ..fields import DjangoConnectionField from .utils import get_filtering_args_from_filterset, get_filterset_class @@ -92,6 +94,16 @@ def filter_kwargs(): qs = super().resolve_queryset(connection, iterable, info, args) + if info.is_awaitable(qs): + async def filter_async(): + filterset = filterset_class( + data=filter_kwargs(), queryset=await qs, request=info.context + ) + if await sync_to_async(filterset.is_valid)(): + return filterset.qs + raise ValidationError(filterset.form.errors.as_json()) + return filter_async() + filterset = filterset_class( data=filter_kwargs(), queryset=qs, request=info.context ) From ebbc5784357be404ec58de39351280cd2afed314 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Fri, 31 Mar 2023 11:48:11 -0700 Subject: [PATCH 07/35] Strange scope --- graphene_django/fields.py | 4 ++-- graphene_django/filter/fields.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index c8899de2f..6e1b0b1de 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -240,7 +240,7 @@ def connection_resolver( iterable = resolver(root, info, **args) if info.is_awaitable(iterable): - async def resolve_connection_async(): + async def resolve_connection_async(iterable): iterable = await iterable if iterable is None: iterable = default_manager @@ -251,7 +251,7 @@ async def resolve_connection_async(): iterable = await iterable return await sync_to_async(cls.resolve_connection)(connection, args, iterable, max_limit=max_limit) - return resolve_connection_async() + return resolve_connection_async(iterable) if iterable is None: iterable = default_manager diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index a14146058..c62ee9ca9 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -95,14 +95,14 @@ def filter_kwargs(): qs = super().resolve_queryset(connection, iterable, info, args) if info.is_awaitable(qs): - async def filter_async(): + async def filter_async(qs): filterset = filterset_class( data=filter_kwargs(), queryset=await qs, request=info.context ) if await sync_to_async(filterset.is_valid)(): return filterset.qs raise ValidationError(filterset.form.errors.as_json()) - return filter_async() + return filter_async(qs) filterset = filterset_class( data=filter_kwargs(), queryset=qs, request=info.context From 66938e9e291d48b73f81f3bdb108bcc46f7a0317 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Fri, 31 Mar 2023 11:49:16 -0700 Subject: [PATCH 08/35] async hates csrf --- graphene_django/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphene_django/views.py b/graphene_django/views.py index 1ea6db8ae..a98a8c50d 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -485,7 +485,6 @@ def as_view(cls, **initkwargs): view._is_coroutine = coroutines._is_coroutine return view - @method_decorator(ensure_csrf_cookie) async def dispatch(self, request, *args, **kwargs): try: if request.method.lower() not in ("get", "post"): From 1b2d5e02e42849c65432425a23026e61b893aaa6 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Fri, 31 Mar 2023 14:04:37 -0700 Subject: [PATCH 09/35] handle async serlizer mutations --- graphene_django/rest_framework/mutation.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index 4062a4423..8cdd4701c 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -2,6 +2,8 @@ from django.shortcuts import get_object_or_404 from rest_framework import serializers +from asyncio import get_running_loop +from asgiref.sync import sync_to_async import graphene from graphene.relay.mutation import ClientIDMutation @@ -152,6 +154,19 @@ def mutate_and_get_payload(cls, root, info, **input): kwargs = cls.get_serializer_kwargs(root, info, **input) serializer = cls._meta.serializer_class(**kwargs) + try: + get_running_loop() + except RuntimeError: + pass + else: + async def perform_mutate_async(): + if await sync_to_async(serializer.is_valid)(): + return await sync_to_async(cls.perform_mutate)(serializer, info) + else: + errors = ErrorType.from_errors(serializer.errors) + return cls(errors=errors) + return perform_mutate_async() + if serializer.is_valid(): return cls.perform_mutate(serializer, info) else: From 0a84a6ea38455906f2e978863a1503a272687f4b Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Sat, 1 Apr 2023 19:16:12 -0700 Subject: [PATCH 10/35] Handle async get_node --- graphene_django/converter.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index dbd7cb8db..90c2128df 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -265,8 +265,21 @@ def dynamic_type(): _type = registry.get_type_for_model(model) if not _type: return + + class CustomField(Field): + def wrap_resolve(self, parent_resolver): + resolver = super().wrap_resolve(parent_resolver) - return Field(_type, required=not field.null) + try: + get_running_loop() + except RuntimeError: + pass + else: + resolver=sync_to_async(resolver) + + return resolver + + return CustomField(_type, required=not field.null) return Dynamic(dynamic_type) From 64d311d770a2c171300854fc8dd64f33b111fa0d Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Sat, 1 Apr 2023 19:54:50 -0700 Subject: [PATCH 11/35] Copy tests for query to test async execution --- graphene_django/tests/test_query_async.py | 1772 +++++++++++++++++++++ setup.py | 1 + 2 files changed, 1773 insertions(+) create mode 100644 graphene_django/tests/test_query_async.py diff --git a/graphene_django/tests/test_query_async.py b/graphene_django/tests/test_query_async.py new file mode 100644 index 000000000..44eaeb6de --- /dev/null +++ b/graphene_django/tests/test_query_async.py @@ -0,0 +1,1772 @@ +import datetime +import base64 + +from django.db import models +from django.db.models import Q +from django.utils.functional import SimpleLazyObject +from graphql_relay import to_global_id +from pytest import raises, mark +from asgiref.sync import sync_to_async + +import graphene +from graphene.relay import Node + +from ..compat import IntegerRangeField, MissingType +from ..fields import DjangoConnectionField +from ..types import DjangoObjectType +from ..utils import DJANGO_FILTER_INSTALLED +from .models import Article, CNNReporter, Film, FilmDetails, Person, Pet, Reporter + +@mark.asyncio +async def test_should_query_only_fields(): + with raises(Exception): + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + fields = ("articles",) + + schema = graphene.Schema(query=ReporterType) + query = """ + query ReporterQuery { + articles + } + """ + result = await schema.execute_async(query) + assert not result.errors + +@mark.asyncio +async def test_should_query_simplelazy_objects(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + fields = ("id",) + + class Query(graphene.ObjectType): + reporter = graphene.Field(ReporterType) + + def resolve_reporter(self, info): + return SimpleLazyObject(lambda: Reporter(id=1)) + + schema = graphene.Schema(query=Query) + query = """ + query { + reporter { + id + } + } + """ + result = await schema.execute_async(query) + assert not result.errors + assert result.data == {"reporter": {"id": "1"}} + +@mark.asyncio +async def test_should_query_wrapped_simplelazy_objects(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + fields = ("id",) + + class Query(graphene.ObjectType): + reporter = graphene.Field(ReporterType) + + def resolve_reporter(self, info): + return SimpleLazyObject(lambda: SimpleLazyObject(lambda: Reporter(id=1))) + + schema = graphene.Schema(query=Query) + query = """ + query { + reporter { + id + } + } + """ + result = await schema.execute_async(query) + assert not result.errors + assert result.data == {"reporter": {"id": "1"}} + +@mark.asyncio +async def test_should_query_well(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + fields = "__all__" + + class Query(graphene.ObjectType): + reporter = graphene.Field(ReporterType) + + def resolve_reporter(self, info): + return Reporter(first_name="ABA", last_name="X") + + query = """ + query ReporterQuery { + reporter { + firstName, + lastName, + email + } + } + """ + expected = {"reporter": {"firstName": "ABA", "lastName": "X", "email": ""}} + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + +@mark.asyncio +@mark.skipif(IntegerRangeField is MissingType, reason="RangeField should exist") +async def test_should_query_postgres_fields(): + from django.contrib.postgres.fields import ( + IntegerRangeField, + ArrayField, + JSONField, + HStoreField, + ) + + class Event(models.Model): + ages = IntegerRangeField(help_text="The age ranges") + data = JSONField(help_text="Data") + store = HStoreField() + tags = ArrayField(models.CharField(max_length=50)) + + class EventType(DjangoObjectType): + class Meta: + model = Event + fields = "__all__" + + class Query(graphene.ObjectType): + event = graphene.Field(EventType) + + def resolve_event(self, info): + return Event( + ages=(0, 10), + data={"angry_babies": True}, + store={"h": "store"}, + tags=["child", "angry", "babies"], + ) + + schema = graphene.Schema(query=Query) + query = """ + query myQuery { + event { + ages + tags + data + store + } + } + """ + expected = { + "event": { + "ages": [0, 10], + "tags": ["child", "angry", "babies"], + "data": '{"angry_babies": true}', + "store": '{"h": "store"}', + } + } + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + +@mark.asyncio +async def test_should_node(): + class ReporterNode(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + @classmethod + def get_node(cls, info, id): + return Reporter(id=2, first_name="Cookie Monster") + + def resolve_articles(self, info, **args): + return [Article(headline="Hi!")] + + class ArticleNode(DjangoObjectType): + class Meta: + model = Article + interfaces = (Node,) + fields = "__all__" + + @classmethod + def get_node(cls, info, id): + return Article( + id=1, headline="Article node", pub_date=datetime.date(2002, 3, 11) + ) + + class Query(graphene.ObjectType): + node = Node.Field() + reporter = graphene.Field(ReporterNode) + article = graphene.Field(ArticleNode) + + def resolve_reporter(self, info): + return Reporter(id=1, first_name="ABA", last_name="X") + + query = """ + query ReporterQuery { + reporter { + id, + firstName, + articles { + edges { + node { + headline + } + } + } + lastName, + email + } + myArticle: node(id:"QXJ0aWNsZU5vZGU6MQ==") { + id + ... on ReporterNode { + firstName + } + ... on ArticleNode { + headline + pubDate + } + } + } + """ + expected = { + "reporter": { + "id": "UmVwb3J0ZXJOb2RlOjE=", + "firstName": "ABA", + "lastName": "X", + "email": "", + "articles": {"edges": [{"node": {"headline": "Hi!"}}]}, + }, + "myArticle": { + "id": "QXJ0aWNsZU5vZGU6MQ==", + "headline": "Article node", + "pubDate": "2002-03-11", + }, + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + +@mark.asyncio +async def test_should_query_onetoone_fields(): + film = Film.objects.create(id=1) + film_details = FilmDetails.objects.create(id=1, film=film) + + class FilmNode(DjangoObjectType): + class Meta: + model = Film + interfaces = (Node,) + fields = "__all__" + + class FilmDetailsNode(DjangoObjectType): + class Meta: + model = FilmDetails + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + film = graphene.Field(FilmNode) + film_details = graphene.Field(FilmDetailsNode) + + def resolve_film(root, info): + return film + + def resolve_film_details(root, info): + return film_details + + query = """ + query FilmQuery { + filmDetails { + id + film { + id + } + } + film { + id + details { + id + } + } + } + """ + expected = { + "filmDetails": { + "id": "RmlsbURldGFpbHNOb2RlOjE=", + "film": {"id": "RmlsbU5vZGU6MQ=="}, + }, + "film": { + "id": "RmlsbU5vZGU6MQ==", + "details": {"id": "RmlsbURldGFpbHNOb2RlOjE="}, + }, + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + +@mark.asyncio +async def test_should_query_connectionfields(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = ("articles",) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + def resolve_all_reporters(self, info, **args): + return [Reporter(id=1)] + + schema = graphene.Schema(query=Query) + query = """ + query ReporterConnectionQuery { + allReporters { + pageInfo { + hasNextPage + } + edges { + node { + id + } + } + } + } + """ + result = await schema.execute_async(query) + assert not result.errors + assert result.data == { + "allReporters": { + "pageInfo": {"hasNextPage": False}, + "edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}], + } + } + +@mark.asyncio +async def test_should_keep_annotations(): + from django.db.models import Count, Avg + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = ("articles",) + + class ArticleType(DjangoObjectType): + class Meta: + model = Article + interfaces = (Node,) + fields = "__all__" + filter_fields = ("lang",) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + all_articles = DjangoConnectionField(ArticleType) + + @staticmethod + @sync_to_async + def resolve_all_reporters(self, info, **args): + return Reporter.objects.annotate(articles_c=Count("articles")).order_by( + "articles_c" + ) + + @staticmethod + @sync_to_async + def resolve_all_articles(self, info, **args): + return Article.objects.annotate(import_avg=Avg("importance")).order_by( + "import_avg" + ) + + schema = graphene.Schema(query=Query) + query = """ + query ReporterConnectionQuery { + allReporters { + pageInfo { + hasNextPage + } + edges { + node { + id + } + } + } + allArticles { + pageInfo { + hasNextPage + } + edges { + node { + id + } + } + } + } + """ + result = await schema.execute_async(query) + assert not result.errors + + +@mark.skipif( + not DJANGO_FILTER_INSTALLED, reason="django-filter should be installed") +@mark.asyncio +async def test_should_query_node_filtering(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class ArticleType(DjangoObjectType): + class Meta: + model = Article + interfaces = (Node,) + fields = "__all__" + filter_fields = ("lang",) + convert_choices_to_enum = False + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + Article.objects.create( + headline="Article Node 1", + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + reporter=r, + editor=r, + lang="es", + ) + Article.objects.create( + headline="Article Node 2", + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + reporter=r, + editor=r, + lang="en", + ) + + schema = graphene.Schema(query=Query) + query = """ + query NodeFilteringQuery { + allReporters { + edges { + node { + id + articles(lang: "es") { + edges { + node { + id + } + } + } + } + } + } + } + """ + + expected = { + "allReporters": { + "edges": [ + { + "node": { + "id": "UmVwb3J0ZXJUeXBlOjE=", + "articles": { + "edges": [{"node": {"id": "QXJ0aWNsZVR5cGU6MQ=="}}] + }, + } + } + ] + } + } + + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@mark.skipif( + not DJANGO_FILTER_INSTALLED, reason="django-filter should be installed") +@mark.asyncio +async def test_should_query_node_filtering_with_distinct_queryset(): + class FilmType(DjangoObjectType): + class Meta: + model = Film + interfaces = (Node,) + fields = "__all__" + filter_fields = ("genre",) + + class Query(graphene.ObjectType): + films = DjangoConnectionField(FilmType) + + # def resolve_all_reporters_with_berlin_films(self, args, context, info): + # return Reporter.objects.filter(Q(films__film__location__contains="Berlin") | Q(a_choice=1)) + + @sync_to_async + def resolve_films(self, info, **args): + return Film.objects.filter( + Q(details__location__contains="Berlin") | Q(genre__in=["ot"]) + ).distinct() + + f = Film.objects.create() + fd = FilmDetails.objects.create(location="Berlin", film=f) + + schema = graphene.Schema(query=Query) + query = """ + query NodeFilteringQuery { + films { + edges { + node { + genre + } + } + } + } + """ + + expected = {"films": {"edges": [{"node": {"genre": "OT"}}]}} + + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@mark.skipif(not DJANGO_FILTER_INSTALLED, reason="django-filter should be installed") +@mark.asyncio +async def test_should_query_node_multiple_filtering(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class ArticleType(DjangoObjectType): + class Meta: + model = Article + interfaces = (Node,) + fields = "__all__" + filter_fields = ("lang", "headline") + convert_choices_to_enum = False + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + Article.objects.create( + headline="Article Node 1", + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + reporter=r, + editor=r, + lang="es", + ) + Article.objects.create( + headline="Article Node 2", + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + reporter=r, + editor=r, + lang="es", + ) + Article.objects.create( + headline="Article Node 3", + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + reporter=r, + editor=r, + lang="en", + ) + + schema = graphene.Schema(query=Query) + query = """ + query NodeFilteringQuery { + allReporters { + edges { + node { + id + articles(lang: "es", headline: "Article Node 1") { + edges { + node { + id + } + } + } + } + } + } + } + """ + + expected = { + "allReporters": { + "edges": [ + { + "node": { + "id": "UmVwb3J0ZXJUeXBlOjE=", + "articles": { + "edges": [{"node": {"id": "QXJ0aWNsZVR5cGU6MQ=="}}] + }, + } + } + ] + } + } + + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + +@mark.asyncio +async def test_should_enforce_first_or_last(graphene_settings): + graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = True + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + + schema = graphene.Schema(query=Query) + query = """ + query NodeFilteringQuery { + allReporters { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": None} + + result = await schema.execute_async(query) + assert len(result.errors) == 1 + assert str(result.errors[0]).startswith( + "You must provide a `first` or `last` value to properly " + "paginate the `allReporters` connection.\n" + ) + assert result.data == expected + +@mark.asyncio +async def test_should_error_if_first_is_greater_than_max(graphene_settings): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + assert Query.all_reporters.max_limit == 100 + + r = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + + schema = graphene.Schema(query=Query) + query = """ + query NodeFilteringQuery { + allReporters(first: 101) { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": None} + + result = await schema.execute_async(query) + assert len(result.errors) == 1 + assert str(result.errors[0]).startswith( + "Requesting 101 records on the `allReporters` connection " + "exceeds the `first` limit of 100 records.\n" + ) + assert result.data == expected + +@mark.asyncio +async def test_should_error_if_last_is_greater_than_max(graphene_settings): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + assert Query.all_reporters.max_limit == 100 + + r = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + + schema = graphene.Schema(query=Query) + query = """ + query NodeFilteringQuery { + allReporters(last: 101) { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": None} + + result = await schema.execute_async(query) + assert len(result.errors) == 1 + assert str(result.errors[0]).startswith( + "Requesting 101 records on the `allReporters` connection " + "exceeds the `last` limit of 100 records.\n" + ) + assert result.data == expected + +@mark.asyncio +async def test_should_query_promise_connectionfields(): + from promise import Promise + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + def resolve_all_reporters(self, info, **args): + return Promise.resolve([Reporter(id=1)]).get() + + schema = graphene.Schema(query=Query) + query = """ + query ReporterPromiseConnectionQuery { + allReporters(first: 1) { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} + + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + +@mark.asyncio +async def test_should_query_connectionfields_with_last(): + + r = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + def resolve_all_reporters(self, info, **args): + return Reporter.objects.all() + + schema = graphene.Schema(query=Query) + query = """ + query ReporterLastQuery { + allReporters(last: 1) { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} + + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + +@mark.asyncio +async def test_should_query_connectionfields_with_manager(): + + r = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + + r = Reporter.objects.create( + first_name="John", last_name="NotDoe", email="johndoe@example.com", a_choice=1 + ) + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType, on="doe_objects") + + def resolve_all_reporters(self, info, **args): + return Reporter.objects.all() + + schema = graphene.Schema(query=Query) + query = """ + query ReporterLastQuery { + allReporters(first: 1) { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} + + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + +@mark.asyncio +async def test_should_query_dataloader_fields(): + from promise import Promise + from promise.dataloader import DataLoader + + def article_batch_load_fn(keys): + queryset = Article.objects.filter(reporter_id__in=keys) + return Promise.resolve( + [ + [article for article in queryset if article.reporter_id == id] + for id in keys + ] + ) + + article_loader = DataLoader(article_batch_load_fn) + + class ArticleType(DjangoObjectType): + class Meta: + model = Article + interfaces = (Node,) + fields = "__all__" + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + use_connection = True + fields = "__all__" + + articles = DjangoConnectionField(ArticleType) + + def resolve_articles(self, info, **args): + return article_loader.load(self.id).get() + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + + Article.objects.create( + headline="Article Node 1", + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + reporter=r, + editor=r, + lang="es", + ) + Article.objects.create( + headline="Article Node 2", + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + reporter=r, + editor=r, + lang="en", + ) + + schema = graphene.Schema(query=Query) + query = """ + query ReporterPromiseConnectionQuery { + allReporters(first: 1) { + edges { + node { + id + articles(first: 2) { + edges { + node { + headline + } + } + } + } + } + } + } + """ + + expected = { + "allReporters": { + "edges": [ + { + "node": { + "id": "UmVwb3J0ZXJUeXBlOjE=", + "articles": { + "edges": [ + {"node": {"headline": "Article Node 1"}}, + {"node": {"headline": "Article Node 2"}}, + ] + }, + } + } + ] + } + } + + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + +@mark.asyncio +async def test_should_handle_inherited_choices(): + class BaseModel(models.Model): + choice_field = models.IntegerField(choices=((0, "zero"), (1, "one"))) + + class ChildModel(BaseModel): + class Meta: + proxy = True + + class BaseType(DjangoObjectType): + class Meta: + model = BaseModel + fields = "__all__" + + class ChildType(DjangoObjectType): + class Meta: + model = ChildModel + fields = "__all__" + + class Query(graphene.ObjectType): + base = graphene.Field(BaseType) + child = graphene.Field(ChildType) + + schema = graphene.Schema(query=Query) + query = """ + query { + child { + choiceField + } + } + """ + result = await schema.execute_async(query) + assert not result.errors + +@mark.asyncio +async def test_proxy_model_support(): + """ + This test asserts that we can query for all Reporters and proxied Reporters. + """ + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + use_connection = True + fields = "__all__" + + class CNNReporterType(DjangoObjectType): + class Meta: + model = CNNReporter + interfaces = (Node,) + use_connection = True + fields = "__all__" + + reporter = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + + cnn_reporter = CNNReporter.objects.create( + first_name="Some", + last_name="Guy", + email="someguy@cnn.com", + a_choice=1, + reporter_type=2, # set this guy to be CNN + ) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + cnn_reporters = DjangoConnectionField(CNNReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query ProxyModelQuery { + allReporters { + edges { + node { + id + } + } + } + cnnReporters { + edges { + node { + id + } + } + } + } + """ + + expected = { + "allReporters": { + "edges": [ + {"node": {"id": to_global_id("ReporterType", reporter.id)}}, + {"node": {"id": to_global_id("ReporterType", cnn_reporter.id)}}, + ] + }, + "cnnReporters": { + "edges": [ + {"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}} + ] + }, + } + + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + +@mark.asyncio +async def test_should_resolve_get_queryset_connectionfields(): + reporter_1 = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + reporter_2 = CNNReporter.objects.create( + first_name="Some", + last_name="Guy", + email="someguy@cnn.com", + a_choice=1, + reporter_type=2, # set this guy to be CNN + ) + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + @classmethod + def get_queryset(cls, queryset, info): + return queryset.filter(reporter_type=2) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query ReporterPromiseConnectionQuery { + allReporters(first: 1) { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}}]}} + + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + +@mark.asyncio +async def test_connection_should_limit_after_to_list_length(): + reporter_1 = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + reporter_2 = Reporter.objects.create( + first_name="Some", last_name="Guy", email="someguy@cnn.com", a_choice=1 + ) + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query ReporterPromiseConnectionQuery ($after: String) { + allReporters(first: 1 after: $after) { + edges { + node { + id + } + } + } + } + """ + + after = base64.b64encode(b"arrayconnection:10").decode() + result = await schema.execute_async(query, variable_values=dict(after=after)) + expected = {"allReporters": {"edges": []}} + assert not result.errors + assert result.data == expected + + +REPORTERS = [ + dict( + first_name=f"First {i}", + last_name=f"Last {i}", + email=f"johndoe+{i}@example.com", + a_choice=1, + ) + for i in range(6) +] + +@mark.asyncio +async def test_should_return_max_limit(graphene_settings): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 4 + reporters = [Reporter(**kwargs) for kwargs in REPORTERS] + Reporter.objects.bulk_create(reporters) + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query AllReporters { + allReporters { + edges { + node { + id + } + } + } + } + """ + + result = await schema.execute_async(query) + assert not result.errors + assert len(result.data["allReporters"]["edges"]) == 4 + +@mark.asyncio +async def test_should_have_next_page(graphene_settings): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 4 + reporters = [Reporter(**kwargs) for kwargs in REPORTERS] + await Reporter.objects.abulk_create(reporters) + db_reporters = await sync_to_async(Reporter.objects.all)() + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query AllReporters($first: Int, $after: String) { + allReporters(first: $first, after: $after) { + pageInfo { + hasNextPage + endCursor + } + edges { + node { + id + } + } + } + } + """ + + result = await schema.execute_async(query, variable_values={}) + assert not result.errors + assert len(result.data["allReporters"]["edges"]) == 4 + assert result.data["allReporters"]["pageInfo"]["hasNextPage"] + + last_result = result.data["allReporters"]["pageInfo"]["endCursor"] + result2 = await schema.execute_async(query, variable_values=dict(first=4, after=last_result)) + assert not result2.errors + assert len(result2.data["allReporters"]["edges"]) == 2 + assert not result2.data["allReporters"]["pageInfo"]["hasNextPage"] + gql_reporters = ( + result.data["allReporters"]["edges"] + result2.data["allReporters"]["edges"] + ) + + def get_test(): + assert {to_global_id("ReporterType", reporter.id) for reporter in db_reporters} == { + gql_reporter["node"]["id"] for gql_reporter in gql_reporters + } + await sync_to_async(get_test)() + +@mark.parametrize("max_limit", [100, 4]) +class TestBackwardPagination: + def setup_schema(self, graphene_settings, max_limit): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit + reporters = [Reporter(**kwargs) for kwargs in REPORTERS] + Reporter.objects.bulk_create(reporters) + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + return schema + @mark.asyncio + async def test_query_last(self, graphene_settings, max_limit): + schema = self.setup_schema(graphene_settings, max_limit=max_limit) + query_last = """ + query { + allReporters(last: 3) { + edges { + node { + firstName + } + } + } + } + """ + + result = await schema.execute_async(query_last) + assert not result.errors + assert len(result.data["allReporters"]["edges"]) == 3 + assert [ + e["node"]["firstName"] for e in result.data["allReporters"]["edges"] + ] == ["First 3", "First 4", "First 5"] + @mark.asyncio + async def test_query_first_and_last(self, graphene_settings, max_limit): + schema = self.setup_schema(graphene_settings, max_limit=max_limit) + query_first_and_last = """ + query { + allReporters(first: 4, last: 3) { + edges { + node { + firstName + } + } + } + } + """ + + result = await schema.execute_async(query_first_and_last) + assert not result.errors + assert len(result.data["allReporters"]["edges"]) == 3 + assert [ + e["node"]["firstName"] for e in result.data["allReporters"]["edges"] + ] == ["First 1", "First 2", "First 3"] + @mark.asyncio + async def test_query_first_last_and_after(self, graphene_settings, max_limit): + schema = self.setup_schema(graphene_settings, max_limit=max_limit) + query_first_last_and_after = """ + query queryAfter($after: String) { + allReporters(first: 4, last: 3, after: $after) { + edges { + node { + firstName + } + } + } + } + """ + + after = base64.b64encode(b"arrayconnection:0").decode() + result = await schema.execute_async( + query_first_last_and_after, + variable_values=dict(after=after), + ) + assert not result.errors + assert len(result.data["allReporters"]["edges"]) == 3 + assert [ + e["node"]["firstName"] for e in result.data["allReporters"]["edges"] + ] == ["First 2", "First 3", "First 4"] + @mark.asyncio + async def test_query_last_and_before(self, graphene_settings, max_limit): + schema = self.setup_schema(graphene_settings, max_limit=max_limit) + query_first_last_and_after = """ + query queryAfter($before: String) { + allReporters(last: 1, before: $before) { + edges { + node { + firstName + } + } + } + } + """ + + result = await schema.execute_async( + query_first_last_and_after, + ) + assert not result.errors + assert len(result.data["allReporters"]["edges"]) == 1 + assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 5" + + before = base64.b64encode(b"arrayconnection:5").decode() + result = await schema.execute_async( + query_first_last_and_after, + variable_values=dict(before=before), + ) + assert not result.errors + assert len(result.data["allReporters"]["edges"]) == 1 + assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 4" + +@mark.asyncio +async def test_should_preserve_prefetch_related(django_assert_num_queries): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (graphene.relay.Node,) + fields = "__all__" + + class FilmType(DjangoObjectType): + reporters = DjangoConnectionField(ReporterType) + + class Meta: + model = Film + interfaces = (graphene.relay.Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + films = DjangoConnectionField(FilmType) + + def resolve_films(root, info, **kwargs): + qs = Film.objects.prefetch_related("reporters") + return qs + + r1 = Reporter.objects.create(first_name="Dave", last_name="Smith") + r2 = Reporter.objects.create(first_name="Jane", last_name="Doe") + + f1 = Film.objects.create() + f1.reporters.set([r1, r2]) + f2 = Film.objects.create() + f2.reporters.set([r2]) + + query = """ + query { + films { + edges { + node { + reporters { + edges { + node { + firstName + } + } + } + } + } + } + } + """ + schema = graphene.Schema(query=Query) + + with django_assert_num_queries(3) as captured: + result = await schema.execute_async(query) + assert not result.errors + +@mark.asyncio +async def test_should_preserve_annotations(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (graphene.relay.Node,) + fields = "__all__" + + class FilmType(DjangoObjectType): + reporters = DjangoConnectionField(ReporterType) + reporters_count = graphene.Int() + + class Meta: + model = Film + interfaces = (graphene.relay.Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + films = DjangoConnectionField(FilmType) + + def resolve_films(root, info, **kwargs): + qs = Film.objects.prefetch_related("reporters") + return qs.annotate(reporters_count=models.Count("reporters")) + + r1 = Reporter.objects.create(first_name="Dave", last_name="Smith") + r2 = Reporter.objects.create(first_name="Jane", last_name="Doe") + + f1 = Film.objects.create() + f1.reporters.set([r1, r2]) + f2 = Film.objects.create() + f2.reporters.set([r2]) + + query = """ + query { + films { + edges { + node { + reportersCount + } + } + } + } + """ + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors, str(result) + + expected = { + "films": { + "edges": [{"node": {"reportersCount": 2}}, {"node": {"reportersCount": 1}}] + } + } + assert result.data == expected, str(result.data) + assert not result.errors + +@mark.asyncio +async def test_connection_should_enable_offset_filtering(): + Reporter.objects.create(first_name="John", last_name="Doe") + Reporter.objects.create(first_name="Some", last_name="Guy") + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query { + allReporters(first: 1, offset: 1) { + edges { + node { + firstName + lastName + } + } + } + } + """ + + result = await schema.execute_async(query) + assert not result.errors + expected = { + "allReporters": { + "edges": [ + {"node": {"firstName": "Some", "lastName": "Guy"}}, + ] + } + } + assert result.data == expected + +@mark.asyncio +async def test_connection_should_enable_offset_filtering_higher_than_max_limit( + graphene_settings, +): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 2 + Reporter.objects.create(first_name="John", last_name="Doe") + Reporter.objects.create(first_name="Some", last_name="Guy") + Reporter.objects.create(first_name="Jane", last_name="Roe") + Reporter.objects.create(first_name="Some", last_name="Lady") + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query { + allReporters(first: 1, offset: 3) { + edges { + node { + firstName + lastName + } + } + } + } + """ + + result = await schema.execute_async(query) + assert not result.errors + expected = { + "allReporters": { + "edges": [ + {"node": {"firstName": "Some", "lastName": "Lady"}}, + ] + } + } + assert result.data == expected + +@mark.asyncio +async def test_connection_should_forbid_offset_filtering_with_before(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query ReporterPromiseConnectionQuery ($before: String) { + allReporters(first: 1, before: $before, offset: 1) { + edges { + node { + firstName + lastName + } + } + } + } + """ + before = base64.b64encode(b"arrayconnection:2").decode() + result = await schema.execute_async(query, variable_values=dict(before=before)) + expected_error = "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `allReporters` connection." + assert len(result.errors) == 1 + assert result.errors[0].message == expected_error + +@mark.asyncio +async def test_connection_should_allow_offset_filtering_with_after(): + Reporter.objects.create(first_name="John", last_name="Doe") + Reporter.objects.create(first_name="Some", last_name="Guy") + Reporter.objects.create(first_name="Jane", last_name="Roe") + Reporter.objects.create(first_name="Some", last_name="Lady") + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query ReporterPromiseConnectionQuery ($after: String) { + allReporters(first: 1, after: $after, offset: 1) { + edges { + node { + firstName + lastName + } + } + } + } + """ + + after = base64.b64encode(b"arrayconnection:0").decode() + result = await schema.execute_async(query, variable_values=dict(after=after)) + assert not result.errors + expected = { + "allReporters": { + "edges": [ + {"node": {"firstName": "Jane", "lastName": "Roe"}}, + ] + } + } + assert result.data == expected + +@mark.asyncio +async def test_connection_should_succeed_if_last_higher_than_number_of_objects(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + fields = "__all__" + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query ReporterPromiseConnectionQuery ($last: Int) { + allReporters(last: $last) { + edges { + node { + firstName + lastName + } + } + } + } + """ + + result = await schema.execute_async(query, variable_values=dict(last=2)) + assert not result.errors + expected = {"allReporters": {"edges": []}} + assert result.data == expected + + Reporter.objects.create(first_name="John", last_name="Doe") + Reporter.objects.create(first_name="Some", last_name="Guy") + Reporter.objects.create(first_name="Jane", last_name="Roe") + Reporter.objects.create(first_name="Some", last_name="Lady") + + result = await schema.execute_async(query, variable_values=dict(last=2)) + assert not result.errors + expected = { + "allReporters": { + "edges": [ + {"node": {"firstName": "Jane", "lastName": "Roe"}}, + {"node": {"firstName": "Some", "lastName": "Lady"}}, + ] + } + } + assert result.data == expected + + result = await schema.execute_async(query, variable_values=dict(last=4)) + assert not result.errors + expected = { + "allReporters": { + "edges": [ + {"node": {"firstName": "John", "lastName": "Doe"}}, + {"node": {"firstName": "Some", "lastName": "Guy"}}, + {"node": {"firstName": "Jane", "lastName": "Roe"}}, + {"node": {"firstName": "Some", "lastName": "Lady"}}, + ] + } + } + assert result.data == expected + + result = await schema.execute_async(query, variable_values=dict(last=20)) + assert not result.errors + expected = { + "allReporters": { + "edges": [ + {"node": {"firstName": "John", "lastName": "Doe"}}, + {"node": {"firstName": "Some", "lastName": "Guy"}}, + {"node": {"firstName": "Jane", "lastName": "Roe"}}, + {"node": {"firstName": "Some", "lastName": "Lady"}}, + ] + } + } + assert result.data == expected + +@mark.asyncio +async def test_should_query_nullable_foreign_key(): + class PetType(DjangoObjectType): + class Meta: + model = Pet + + class PersonType(DjangoObjectType): + class Meta: + model = Person + + class Query(graphene.ObjectType): + pet = graphene.Field(PetType, name=graphene.String(required=True)) + person = graphene.Field(PersonType, name=graphene.String(required=True)) + + @staticmethod + @sync_to_async + def resolve_pet(self, info, name): + return Pet.objects.filter(name=name).first() + + @staticmethod + @sync_to_async + def resolve_person(self, info, name): + return Person.objects.filter(name=name).first() + + schema = graphene.Schema(query=Query) + + person = await Person.objects.acreate(name="Jane") + pets = [ + await Pet.objects.acreate(name="Stray dog", age=1), + await Pet.objects.acreate(name="Jane's dog", owner=person, age=1), + ] + + query_pet = """ + query getPet($name: String!) { + pet(name: $name) { + owner { + name + } + } + } + """ + result = await schema.execute_async(query_pet, variables={"name": "Stray dog"}) + assert not result.errors + assert result.data["pet"] == { + "owner": None, + } + + result = await schema.execute_async(query_pet, variables={"name": "Jane's dog"}) + assert not result.errors + assert result.data["pet"] == { + "owner": {"name": "Jane"}, + } + + query_owner = """ + query getOwner($name: String!) { + person(name: $name) { + pets { + name + } + } + } + """ + result = await schema.execute_async(query_owner, variables={"name": "Jane"}) + assert not result.errors + assert result.data["person"] == { + "pets": [{"name": "Jane's dog"}], + } diff --git a/setup.py b/setup.py index 37b57a839..64273e2a8 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ "pytz", "django-filter>=22.1", "pytest-django>=4.5.2", + "pytest-asyncio>=0.16,<2" ] + rest_framework_require From bdb8e8444629fbd22ff21580bb506d54a925746f Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Thu, 4 May 2023 15:26:33 +0100 Subject: [PATCH 12/35] Update cookbook for async testing --- examples/cookbook/cookbook/recipes/schema.py | 32 +++++++++++++++++++- examples/cookbook/cookbook/urls.py | 6 ++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/examples/cookbook/cookbook/recipes/schema.py b/examples/cookbook/cookbook/recipes/schema.py index ea5ed38f1..82f7f1d6e 100644 --- a/examples/cookbook/cookbook/recipes/schema.py +++ b/examples/cookbook/cookbook/recipes/schema.py @@ -1,16 +1,40 @@ +import asyncio + +from asgiref.sync import sync_to_async + from cookbook.recipes.models import Recipe, RecipeIngredient -from graphene import Node +from graphene import Node, String, Field from graphene_django.filter import DjangoFilterConnectionField from graphene_django.types import DjangoObjectType class RecipeNode(DjangoObjectType): + async_field = String() + class Meta: model = Recipe interfaces = (Node,) fields = "__all__" filter_fields = ["title", "amounts"] + async def resolve_async_field(self, info): + await asyncio.sleep(2) + return "success" + + +class RecipeType(DjangoObjectType): + async_field = String() + + class Meta: + model = Recipe + fields = "__all__" + filter_fields = ["title", "amounts"] + skip_registry = True + + async def resolve_async_field(self, info): + await asyncio.sleep(2) + return "success" + class RecipeIngredientNode(DjangoObjectType): class Meta: @@ -27,7 +51,13 @@ class Meta: class Query: recipe = Node.Field(RecipeNode) + raw_recipe = Field(RecipeType) all_recipes = DjangoFilterConnectionField(RecipeNode) recipeingredient = Node.Field(RecipeIngredientNode) all_recipeingredients = DjangoFilterConnectionField(RecipeIngredientNode) + + @staticmethod + @sync_to_async + def resolve_raw_recipe(self, info): + return Recipe.objects.first() diff --git a/examples/cookbook/cookbook/urls.py b/examples/cookbook/cookbook/urls.py index e9e69cd25..541cd2df7 100644 --- a/examples/cookbook/cookbook/urls.py +++ b/examples/cookbook/cookbook/urls.py @@ -1,9 +1,9 @@ from django.urls import re_path from django.contrib import admin - -from graphene_django.views import GraphQLView +from django.views.decorators.csrf import csrf_exempt +from graphene_django.views import AsyncGraphQLView urlpatterns = [ re_path(r"^admin/", admin.site.urls), - re_path(r"^graphql$", GraphQLView.as_view(graphiql=True)), + re_path(r"^graphql$", csrf_exempt(AsyncGraphQLView.as_view(graphiql=True))), ] From 4d5132d925dbd6448aeca876f93ec68f6740cdf8 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Thu, 4 May 2023 15:26:52 +0100 Subject: [PATCH 13/35] Remove tests for now --- graphene_django/tests/test_query_async.py | 1772 --------------------- 1 file changed, 1772 deletions(-) delete mode 100644 graphene_django/tests/test_query_async.py diff --git a/graphene_django/tests/test_query_async.py b/graphene_django/tests/test_query_async.py deleted file mode 100644 index 44eaeb6de..000000000 --- a/graphene_django/tests/test_query_async.py +++ /dev/null @@ -1,1772 +0,0 @@ -import datetime -import base64 - -from django.db import models -from django.db.models import Q -from django.utils.functional import SimpleLazyObject -from graphql_relay import to_global_id -from pytest import raises, mark -from asgiref.sync import sync_to_async - -import graphene -from graphene.relay import Node - -from ..compat import IntegerRangeField, MissingType -from ..fields import DjangoConnectionField -from ..types import DjangoObjectType -from ..utils import DJANGO_FILTER_INSTALLED -from .models import Article, CNNReporter, Film, FilmDetails, Person, Pet, Reporter - -@mark.asyncio -async def test_should_query_only_fields(): - with raises(Exception): - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - fields = ("articles",) - - schema = graphene.Schema(query=ReporterType) - query = """ - query ReporterQuery { - articles - } - """ - result = await schema.execute_async(query) - assert not result.errors - -@mark.asyncio -async def test_should_query_simplelazy_objects(): - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - fields = ("id",) - - class Query(graphene.ObjectType): - reporter = graphene.Field(ReporterType) - - def resolve_reporter(self, info): - return SimpleLazyObject(lambda: Reporter(id=1)) - - schema = graphene.Schema(query=Query) - query = """ - query { - reporter { - id - } - } - """ - result = await schema.execute_async(query) - assert not result.errors - assert result.data == {"reporter": {"id": "1"}} - -@mark.asyncio -async def test_should_query_wrapped_simplelazy_objects(): - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - fields = ("id",) - - class Query(graphene.ObjectType): - reporter = graphene.Field(ReporterType) - - def resolve_reporter(self, info): - return SimpleLazyObject(lambda: SimpleLazyObject(lambda: Reporter(id=1))) - - schema = graphene.Schema(query=Query) - query = """ - query { - reporter { - id - } - } - """ - result = await schema.execute_async(query) - assert not result.errors - assert result.data == {"reporter": {"id": "1"}} - -@mark.asyncio -async def test_should_query_well(): - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - fields = "__all__" - - class Query(graphene.ObjectType): - reporter = graphene.Field(ReporterType) - - def resolve_reporter(self, info): - return Reporter(first_name="ABA", last_name="X") - - query = """ - query ReporterQuery { - reporter { - firstName, - lastName, - email - } - } - """ - expected = {"reporter": {"firstName": "ABA", "lastName": "X", "email": ""}} - schema = graphene.Schema(query=Query) - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - -@mark.asyncio -@mark.skipif(IntegerRangeField is MissingType, reason="RangeField should exist") -async def test_should_query_postgres_fields(): - from django.contrib.postgres.fields import ( - IntegerRangeField, - ArrayField, - JSONField, - HStoreField, - ) - - class Event(models.Model): - ages = IntegerRangeField(help_text="The age ranges") - data = JSONField(help_text="Data") - store = HStoreField() - tags = ArrayField(models.CharField(max_length=50)) - - class EventType(DjangoObjectType): - class Meta: - model = Event - fields = "__all__" - - class Query(graphene.ObjectType): - event = graphene.Field(EventType) - - def resolve_event(self, info): - return Event( - ages=(0, 10), - data={"angry_babies": True}, - store={"h": "store"}, - tags=["child", "angry", "babies"], - ) - - schema = graphene.Schema(query=Query) - query = """ - query myQuery { - event { - ages - tags - data - store - } - } - """ - expected = { - "event": { - "ages": [0, 10], - "tags": ["child", "angry", "babies"], - "data": '{"angry_babies": true}', - "store": '{"h": "store"}', - } - } - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - -@mark.asyncio -async def test_should_node(): - class ReporterNode(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - @classmethod - def get_node(cls, info, id): - return Reporter(id=2, first_name="Cookie Monster") - - def resolve_articles(self, info, **args): - return [Article(headline="Hi!")] - - class ArticleNode(DjangoObjectType): - class Meta: - model = Article - interfaces = (Node,) - fields = "__all__" - - @classmethod - def get_node(cls, info, id): - return Article( - id=1, headline="Article node", pub_date=datetime.date(2002, 3, 11) - ) - - class Query(graphene.ObjectType): - node = Node.Field() - reporter = graphene.Field(ReporterNode) - article = graphene.Field(ArticleNode) - - def resolve_reporter(self, info): - return Reporter(id=1, first_name="ABA", last_name="X") - - query = """ - query ReporterQuery { - reporter { - id, - firstName, - articles { - edges { - node { - headline - } - } - } - lastName, - email - } - myArticle: node(id:"QXJ0aWNsZU5vZGU6MQ==") { - id - ... on ReporterNode { - firstName - } - ... on ArticleNode { - headline - pubDate - } - } - } - """ - expected = { - "reporter": { - "id": "UmVwb3J0ZXJOb2RlOjE=", - "firstName": "ABA", - "lastName": "X", - "email": "", - "articles": {"edges": [{"node": {"headline": "Hi!"}}]}, - }, - "myArticle": { - "id": "QXJ0aWNsZU5vZGU6MQ==", - "headline": "Article node", - "pubDate": "2002-03-11", - }, - } - schema = graphene.Schema(query=Query) - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - -@mark.asyncio -async def test_should_query_onetoone_fields(): - film = Film.objects.create(id=1) - film_details = FilmDetails.objects.create(id=1, film=film) - - class FilmNode(DjangoObjectType): - class Meta: - model = Film - interfaces = (Node,) - fields = "__all__" - - class FilmDetailsNode(DjangoObjectType): - class Meta: - model = FilmDetails - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - film = graphene.Field(FilmNode) - film_details = graphene.Field(FilmDetailsNode) - - def resolve_film(root, info): - return film - - def resolve_film_details(root, info): - return film_details - - query = """ - query FilmQuery { - filmDetails { - id - film { - id - } - } - film { - id - details { - id - } - } - } - """ - expected = { - "filmDetails": { - "id": "RmlsbURldGFpbHNOb2RlOjE=", - "film": {"id": "RmlsbU5vZGU6MQ=="}, - }, - "film": { - "id": "RmlsbU5vZGU6MQ==", - "details": {"id": "RmlsbURldGFpbHNOb2RlOjE="}, - }, - } - schema = graphene.Schema(query=Query) - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - -@mark.asyncio -async def test_should_query_connectionfields(): - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = ("articles",) - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - def resolve_all_reporters(self, info, **args): - return [Reporter(id=1)] - - schema = graphene.Schema(query=Query) - query = """ - query ReporterConnectionQuery { - allReporters { - pageInfo { - hasNextPage - } - edges { - node { - id - } - } - } - } - """ - result = await schema.execute_async(query) - assert not result.errors - assert result.data == { - "allReporters": { - "pageInfo": {"hasNextPage": False}, - "edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}], - } - } - -@mark.asyncio -async def test_should_keep_annotations(): - from django.db.models import Count, Avg - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = ("articles",) - - class ArticleType(DjangoObjectType): - class Meta: - model = Article - interfaces = (Node,) - fields = "__all__" - filter_fields = ("lang",) - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - all_articles = DjangoConnectionField(ArticleType) - - @staticmethod - @sync_to_async - def resolve_all_reporters(self, info, **args): - return Reporter.objects.annotate(articles_c=Count("articles")).order_by( - "articles_c" - ) - - @staticmethod - @sync_to_async - def resolve_all_articles(self, info, **args): - return Article.objects.annotate(import_avg=Avg("importance")).order_by( - "import_avg" - ) - - schema = graphene.Schema(query=Query) - query = """ - query ReporterConnectionQuery { - allReporters { - pageInfo { - hasNextPage - } - edges { - node { - id - } - } - } - allArticles { - pageInfo { - hasNextPage - } - edges { - node { - id - } - } - } - } - """ - result = await schema.execute_async(query) - assert not result.errors - - -@mark.skipif( - not DJANGO_FILTER_INSTALLED, reason="django-filter should be installed") -@mark.asyncio -async def test_should_query_node_filtering(): - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class ArticleType(DjangoObjectType): - class Meta: - model = Article - interfaces = (Node,) - fields = "__all__" - filter_fields = ("lang",) - convert_choices_to_enum = False - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - r = Reporter.objects.create( - first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 - ) - Article.objects.create( - headline="Article Node 1", - pub_date=datetime.date.today(), - pub_date_time=datetime.datetime.now(), - reporter=r, - editor=r, - lang="es", - ) - Article.objects.create( - headline="Article Node 2", - pub_date=datetime.date.today(), - pub_date_time=datetime.datetime.now(), - reporter=r, - editor=r, - lang="en", - ) - - schema = graphene.Schema(query=Query) - query = """ - query NodeFilteringQuery { - allReporters { - edges { - node { - id - articles(lang: "es") { - edges { - node { - id - } - } - } - } - } - } - } - """ - - expected = { - "allReporters": { - "edges": [ - { - "node": { - "id": "UmVwb3J0ZXJUeXBlOjE=", - "articles": { - "edges": [{"node": {"id": "QXJ0aWNsZVR5cGU6MQ=="}}] - }, - } - } - ] - } - } - - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - - -@mark.skipif( - not DJANGO_FILTER_INSTALLED, reason="django-filter should be installed") -@mark.asyncio -async def test_should_query_node_filtering_with_distinct_queryset(): - class FilmType(DjangoObjectType): - class Meta: - model = Film - interfaces = (Node,) - fields = "__all__" - filter_fields = ("genre",) - - class Query(graphene.ObjectType): - films = DjangoConnectionField(FilmType) - - # def resolve_all_reporters_with_berlin_films(self, args, context, info): - # return Reporter.objects.filter(Q(films__film__location__contains="Berlin") | Q(a_choice=1)) - - @sync_to_async - def resolve_films(self, info, **args): - return Film.objects.filter( - Q(details__location__contains="Berlin") | Q(genre__in=["ot"]) - ).distinct() - - f = Film.objects.create() - fd = FilmDetails.objects.create(location="Berlin", film=f) - - schema = graphene.Schema(query=Query) - query = """ - query NodeFilteringQuery { - films { - edges { - node { - genre - } - } - } - } - """ - - expected = {"films": {"edges": [{"node": {"genre": "OT"}}]}} - - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - - -@mark.skipif(not DJANGO_FILTER_INSTALLED, reason="django-filter should be installed") -@mark.asyncio -async def test_should_query_node_multiple_filtering(): - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class ArticleType(DjangoObjectType): - class Meta: - model = Article - interfaces = (Node,) - fields = "__all__" - filter_fields = ("lang", "headline") - convert_choices_to_enum = False - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - r = Reporter.objects.create( - first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 - ) - Article.objects.create( - headline="Article Node 1", - pub_date=datetime.date.today(), - pub_date_time=datetime.datetime.now(), - reporter=r, - editor=r, - lang="es", - ) - Article.objects.create( - headline="Article Node 2", - pub_date=datetime.date.today(), - pub_date_time=datetime.datetime.now(), - reporter=r, - editor=r, - lang="es", - ) - Article.objects.create( - headline="Article Node 3", - pub_date=datetime.date.today(), - pub_date_time=datetime.datetime.now(), - reporter=r, - editor=r, - lang="en", - ) - - schema = graphene.Schema(query=Query) - query = """ - query NodeFilteringQuery { - allReporters { - edges { - node { - id - articles(lang: "es", headline: "Article Node 1") { - edges { - node { - id - } - } - } - } - } - } - } - """ - - expected = { - "allReporters": { - "edges": [ - { - "node": { - "id": "UmVwb3J0ZXJUeXBlOjE=", - "articles": { - "edges": [{"node": {"id": "QXJ0aWNsZVR5cGU6MQ=="}}] - }, - } - } - ] - } - } - - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - -@mark.asyncio -async def test_should_enforce_first_or_last(graphene_settings): - graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = True - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - r = Reporter.objects.create( - first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 - ) - - schema = graphene.Schema(query=Query) - query = """ - query NodeFilteringQuery { - allReporters { - edges { - node { - id - } - } - } - } - """ - - expected = {"allReporters": None} - - result = await schema.execute_async(query) - assert len(result.errors) == 1 - assert str(result.errors[0]).startswith( - "You must provide a `first` or `last` value to properly " - "paginate the `allReporters` connection.\n" - ) - assert result.data == expected - -@mark.asyncio -async def test_should_error_if_first_is_greater_than_max(graphene_settings): - graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - assert Query.all_reporters.max_limit == 100 - - r = Reporter.objects.create( - first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 - ) - - schema = graphene.Schema(query=Query) - query = """ - query NodeFilteringQuery { - allReporters(first: 101) { - edges { - node { - id - } - } - } - } - """ - - expected = {"allReporters": None} - - result = await schema.execute_async(query) - assert len(result.errors) == 1 - assert str(result.errors[0]).startswith( - "Requesting 101 records on the `allReporters` connection " - "exceeds the `first` limit of 100 records.\n" - ) - assert result.data == expected - -@mark.asyncio -async def test_should_error_if_last_is_greater_than_max(graphene_settings): - graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - assert Query.all_reporters.max_limit == 100 - - r = Reporter.objects.create( - first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 - ) - - schema = graphene.Schema(query=Query) - query = """ - query NodeFilteringQuery { - allReporters(last: 101) { - edges { - node { - id - } - } - } - } - """ - - expected = {"allReporters": None} - - result = await schema.execute_async(query) - assert len(result.errors) == 1 - assert str(result.errors[0]).startswith( - "Requesting 101 records on the `allReporters` connection " - "exceeds the `last` limit of 100 records.\n" - ) - assert result.data == expected - -@mark.asyncio -async def test_should_query_promise_connectionfields(): - from promise import Promise - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - def resolve_all_reporters(self, info, **args): - return Promise.resolve([Reporter(id=1)]).get() - - schema = graphene.Schema(query=Query) - query = """ - query ReporterPromiseConnectionQuery { - allReporters(first: 1) { - edges { - node { - id - } - } - } - } - """ - - expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} - - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - -@mark.asyncio -async def test_should_query_connectionfields_with_last(): - - r = Reporter.objects.create( - first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 - ) - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - def resolve_all_reporters(self, info, **args): - return Reporter.objects.all() - - schema = graphene.Schema(query=Query) - query = """ - query ReporterLastQuery { - allReporters(last: 1) { - edges { - node { - id - } - } - } - } - """ - - expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} - - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - -@mark.asyncio -async def test_should_query_connectionfields_with_manager(): - - r = Reporter.objects.create( - first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 - ) - - r = Reporter.objects.create( - first_name="John", last_name="NotDoe", email="johndoe@example.com", a_choice=1 - ) - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType, on="doe_objects") - - def resolve_all_reporters(self, info, **args): - return Reporter.objects.all() - - schema = graphene.Schema(query=Query) - query = """ - query ReporterLastQuery { - allReporters(first: 1) { - edges { - node { - id - } - } - } - } - """ - - expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} - - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - -@mark.asyncio -async def test_should_query_dataloader_fields(): - from promise import Promise - from promise.dataloader import DataLoader - - def article_batch_load_fn(keys): - queryset = Article.objects.filter(reporter_id__in=keys) - return Promise.resolve( - [ - [article for article in queryset if article.reporter_id == id] - for id in keys - ] - ) - - article_loader = DataLoader(article_batch_load_fn) - - class ArticleType(DjangoObjectType): - class Meta: - model = Article - interfaces = (Node,) - fields = "__all__" - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - use_connection = True - fields = "__all__" - - articles = DjangoConnectionField(ArticleType) - - def resolve_articles(self, info, **args): - return article_loader.load(self.id).get() - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - r = Reporter.objects.create( - first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 - ) - - Article.objects.create( - headline="Article Node 1", - pub_date=datetime.date.today(), - pub_date_time=datetime.datetime.now(), - reporter=r, - editor=r, - lang="es", - ) - Article.objects.create( - headline="Article Node 2", - pub_date=datetime.date.today(), - pub_date_time=datetime.datetime.now(), - reporter=r, - editor=r, - lang="en", - ) - - schema = graphene.Schema(query=Query) - query = """ - query ReporterPromiseConnectionQuery { - allReporters(first: 1) { - edges { - node { - id - articles(first: 2) { - edges { - node { - headline - } - } - } - } - } - } - } - """ - - expected = { - "allReporters": { - "edges": [ - { - "node": { - "id": "UmVwb3J0ZXJUeXBlOjE=", - "articles": { - "edges": [ - {"node": {"headline": "Article Node 1"}}, - {"node": {"headline": "Article Node 2"}}, - ] - }, - } - } - ] - } - } - - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - -@mark.asyncio -async def test_should_handle_inherited_choices(): - class BaseModel(models.Model): - choice_field = models.IntegerField(choices=((0, "zero"), (1, "one"))) - - class ChildModel(BaseModel): - class Meta: - proxy = True - - class BaseType(DjangoObjectType): - class Meta: - model = BaseModel - fields = "__all__" - - class ChildType(DjangoObjectType): - class Meta: - model = ChildModel - fields = "__all__" - - class Query(graphene.ObjectType): - base = graphene.Field(BaseType) - child = graphene.Field(ChildType) - - schema = graphene.Schema(query=Query) - query = """ - query { - child { - choiceField - } - } - """ - result = await schema.execute_async(query) - assert not result.errors - -@mark.asyncio -async def test_proxy_model_support(): - """ - This test asserts that we can query for all Reporters and proxied Reporters. - """ - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - use_connection = True - fields = "__all__" - - class CNNReporterType(DjangoObjectType): - class Meta: - model = CNNReporter - interfaces = (Node,) - use_connection = True - fields = "__all__" - - reporter = Reporter.objects.create( - first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 - ) - - cnn_reporter = CNNReporter.objects.create( - first_name="Some", - last_name="Guy", - email="someguy@cnn.com", - a_choice=1, - reporter_type=2, # set this guy to be CNN - ) - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - cnn_reporters = DjangoConnectionField(CNNReporterType) - - schema = graphene.Schema(query=Query) - query = """ - query ProxyModelQuery { - allReporters { - edges { - node { - id - } - } - } - cnnReporters { - edges { - node { - id - } - } - } - } - """ - - expected = { - "allReporters": { - "edges": [ - {"node": {"id": to_global_id("ReporterType", reporter.id)}}, - {"node": {"id": to_global_id("ReporterType", cnn_reporter.id)}}, - ] - }, - "cnnReporters": { - "edges": [ - {"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}} - ] - }, - } - - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - -@mark.asyncio -async def test_should_resolve_get_queryset_connectionfields(): - reporter_1 = Reporter.objects.create( - first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 - ) - reporter_2 = CNNReporter.objects.create( - first_name="Some", - last_name="Guy", - email="someguy@cnn.com", - a_choice=1, - reporter_type=2, # set this guy to be CNN - ) - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - @classmethod - def get_queryset(cls, queryset, info): - return queryset.filter(reporter_type=2) - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - schema = graphene.Schema(query=Query) - query = """ - query ReporterPromiseConnectionQuery { - allReporters(first: 1) { - edges { - node { - id - } - } - } - } - """ - - expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}}]}} - - result = await schema.execute_async(query) - assert not result.errors - assert result.data == expected - -@mark.asyncio -async def test_connection_should_limit_after_to_list_length(): - reporter_1 = Reporter.objects.create( - first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 - ) - reporter_2 = Reporter.objects.create( - first_name="Some", last_name="Guy", email="someguy@cnn.com", a_choice=1 - ) - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - schema = graphene.Schema(query=Query) - query = """ - query ReporterPromiseConnectionQuery ($after: String) { - allReporters(first: 1 after: $after) { - edges { - node { - id - } - } - } - } - """ - - after = base64.b64encode(b"arrayconnection:10").decode() - result = await schema.execute_async(query, variable_values=dict(after=after)) - expected = {"allReporters": {"edges": []}} - assert not result.errors - assert result.data == expected - - -REPORTERS = [ - dict( - first_name=f"First {i}", - last_name=f"Last {i}", - email=f"johndoe+{i}@example.com", - a_choice=1, - ) - for i in range(6) -] - -@mark.asyncio -async def test_should_return_max_limit(graphene_settings): - graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 4 - reporters = [Reporter(**kwargs) for kwargs in REPORTERS] - Reporter.objects.bulk_create(reporters) - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - schema = graphene.Schema(query=Query) - query = """ - query AllReporters { - allReporters { - edges { - node { - id - } - } - } - } - """ - - result = await schema.execute_async(query) - assert not result.errors - assert len(result.data["allReporters"]["edges"]) == 4 - -@mark.asyncio -async def test_should_have_next_page(graphene_settings): - graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 4 - reporters = [Reporter(**kwargs) for kwargs in REPORTERS] - await Reporter.objects.abulk_create(reporters) - db_reporters = await sync_to_async(Reporter.objects.all)() - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - schema = graphene.Schema(query=Query) - query = """ - query AllReporters($first: Int, $after: String) { - allReporters(first: $first, after: $after) { - pageInfo { - hasNextPage - endCursor - } - edges { - node { - id - } - } - } - } - """ - - result = await schema.execute_async(query, variable_values={}) - assert not result.errors - assert len(result.data["allReporters"]["edges"]) == 4 - assert result.data["allReporters"]["pageInfo"]["hasNextPage"] - - last_result = result.data["allReporters"]["pageInfo"]["endCursor"] - result2 = await schema.execute_async(query, variable_values=dict(first=4, after=last_result)) - assert not result2.errors - assert len(result2.data["allReporters"]["edges"]) == 2 - assert not result2.data["allReporters"]["pageInfo"]["hasNextPage"] - gql_reporters = ( - result.data["allReporters"]["edges"] + result2.data["allReporters"]["edges"] - ) - - def get_test(): - assert {to_global_id("ReporterType", reporter.id) for reporter in db_reporters} == { - gql_reporter["node"]["id"] for gql_reporter in gql_reporters - } - await sync_to_async(get_test)() - -@mark.parametrize("max_limit", [100, 4]) -class TestBackwardPagination: - def setup_schema(self, graphene_settings, max_limit): - graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit - reporters = [Reporter(**kwargs) for kwargs in REPORTERS] - Reporter.objects.bulk_create(reporters) - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - schema = graphene.Schema(query=Query) - return schema - @mark.asyncio - async def test_query_last(self, graphene_settings, max_limit): - schema = self.setup_schema(graphene_settings, max_limit=max_limit) - query_last = """ - query { - allReporters(last: 3) { - edges { - node { - firstName - } - } - } - } - """ - - result = await schema.execute_async(query_last) - assert not result.errors - assert len(result.data["allReporters"]["edges"]) == 3 - assert [ - e["node"]["firstName"] for e in result.data["allReporters"]["edges"] - ] == ["First 3", "First 4", "First 5"] - @mark.asyncio - async def test_query_first_and_last(self, graphene_settings, max_limit): - schema = self.setup_schema(graphene_settings, max_limit=max_limit) - query_first_and_last = """ - query { - allReporters(first: 4, last: 3) { - edges { - node { - firstName - } - } - } - } - """ - - result = await schema.execute_async(query_first_and_last) - assert not result.errors - assert len(result.data["allReporters"]["edges"]) == 3 - assert [ - e["node"]["firstName"] for e in result.data["allReporters"]["edges"] - ] == ["First 1", "First 2", "First 3"] - @mark.asyncio - async def test_query_first_last_and_after(self, graphene_settings, max_limit): - schema = self.setup_schema(graphene_settings, max_limit=max_limit) - query_first_last_and_after = """ - query queryAfter($after: String) { - allReporters(first: 4, last: 3, after: $after) { - edges { - node { - firstName - } - } - } - } - """ - - after = base64.b64encode(b"arrayconnection:0").decode() - result = await schema.execute_async( - query_first_last_and_after, - variable_values=dict(after=after), - ) - assert not result.errors - assert len(result.data["allReporters"]["edges"]) == 3 - assert [ - e["node"]["firstName"] for e in result.data["allReporters"]["edges"] - ] == ["First 2", "First 3", "First 4"] - @mark.asyncio - async def test_query_last_and_before(self, graphene_settings, max_limit): - schema = self.setup_schema(graphene_settings, max_limit=max_limit) - query_first_last_and_after = """ - query queryAfter($before: String) { - allReporters(last: 1, before: $before) { - edges { - node { - firstName - } - } - } - } - """ - - result = await schema.execute_async( - query_first_last_and_after, - ) - assert not result.errors - assert len(result.data["allReporters"]["edges"]) == 1 - assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 5" - - before = base64.b64encode(b"arrayconnection:5").decode() - result = await schema.execute_async( - query_first_last_and_after, - variable_values=dict(before=before), - ) - assert not result.errors - assert len(result.data["allReporters"]["edges"]) == 1 - assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 4" - -@mark.asyncio -async def test_should_preserve_prefetch_related(django_assert_num_queries): - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (graphene.relay.Node,) - fields = "__all__" - - class FilmType(DjangoObjectType): - reporters = DjangoConnectionField(ReporterType) - - class Meta: - model = Film - interfaces = (graphene.relay.Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - films = DjangoConnectionField(FilmType) - - def resolve_films(root, info, **kwargs): - qs = Film.objects.prefetch_related("reporters") - return qs - - r1 = Reporter.objects.create(first_name="Dave", last_name="Smith") - r2 = Reporter.objects.create(first_name="Jane", last_name="Doe") - - f1 = Film.objects.create() - f1.reporters.set([r1, r2]) - f2 = Film.objects.create() - f2.reporters.set([r2]) - - query = """ - query { - films { - edges { - node { - reporters { - edges { - node { - firstName - } - } - } - } - } - } - } - """ - schema = graphene.Schema(query=Query) - - with django_assert_num_queries(3) as captured: - result = await schema.execute_async(query) - assert not result.errors - -@mark.asyncio -async def test_should_preserve_annotations(): - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (graphene.relay.Node,) - fields = "__all__" - - class FilmType(DjangoObjectType): - reporters = DjangoConnectionField(ReporterType) - reporters_count = graphene.Int() - - class Meta: - model = Film - interfaces = (graphene.relay.Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - films = DjangoConnectionField(FilmType) - - def resolve_films(root, info, **kwargs): - qs = Film.objects.prefetch_related("reporters") - return qs.annotate(reporters_count=models.Count("reporters")) - - r1 = Reporter.objects.create(first_name="Dave", last_name="Smith") - r2 = Reporter.objects.create(first_name="Jane", last_name="Doe") - - f1 = Film.objects.create() - f1.reporters.set([r1, r2]) - f2 = Film.objects.create() - f2.reporters.set([r2]) - - query = """ - query { - films { - edges { - node { - reportersCount - } - } - } - } - """ - schema = graphene.Schema(query=Query) - result = await schema.execute_async(query) - assert not result.errors, str(result) - - expected = { - "films": { - "edges": [{"node": {"reportersCount": 2}}, {"node": {"reportersCount": 1}}] - } - } - assert result.data == expected, str(result.data) - assert not result.errors - -@mark.asyncio -async def test_connection_should_enable_offset_filtering(): - Reporter.objects.create(first_name="John", last_name="Doe") - Reporter.objects.create(first_name="Some", last_name="Guy") - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - schema = graphene.Schema(query=Query) - query = """ - query { - allReporters(first: 1, offset: 1) { - edges { - node { - firstName - lastName - } - } - } - } - """ - - result = await schema.execute_async(query) - assert not result.errors - expected = { - "allReporters": { - "edges": [ - {"node": {"firstName": "Some", "lastName": "Guy"}}, - ] - } - } - assert result.data == expected - -@mark.asyncio -async def test_connection_should_enable_offset_filtering_higher_than_max_limit( - graphene_settings, -): - graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 2 - Reporter.objects.create(first_name="John", last_name="Doe") - Reporter.objects.create(first_name="Some", last_name="Guy") - Reporter.objects.create(first_name="Jane", last_name="Roe") - Reporter.objects.create(first_name="Some", last_name="Lady") - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - schema = graphene.Schema(query=Query) - query = """ - query { - allReporters(first: 1, offset: 3) { - edges { - node { - firstName - lastName - } - } - } - } - """ - - result = await schema.execute_async(query) - assert not result.errors - expected = { - "allReporters": { - "edges": [ - {"node": {"firstName": "Some", "lastName": "Lady"}}, - ] - } - } - assert result.data == expected - -@mark.asyncio -async def test_connection_should_forbid_offset_filtering_with_before(): - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - schema = graphene.Schema(query=Query) - query = """ - query ReporterPromiseConnectionQuery ($before: String) { - allReporters(first: 1, before: $before, offset: 1) { - edges { - node { - firstName - lastName - } - } - } - } - """ - before = base64.b64encode(b"arrayconnection:2").decode() - result = await schema.execute_async(query, variable_values=dict(before=before)) - expected_error = "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `allReporters` connection." - assert len(result.errors) == 1 - assert result.errors[0].message == expected_error - -@mark.asyncio -async def test_connection_should_allow_offset_filtering_with_after(): - Reporter.objects.create(first_name="John", last_name="Doe") - Reporter.objects.create(first_name="Some", last_name="Guy") - Reporter.objects.create(first_name="Jane", last_name="Roe") - Reporter.objects.create(first_name="Some", last_name="Lady") - - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - schema = graphene.Schema(query=Query) - query = """ - query ReporterPromiseConnectionQuery ($after: String) { - allReporters(first: 1, after: $after, offset: 1) { - edges { - node { - firstName - lastName - } - } - } - } - """ - - after = base64.b64encode(b"arrayconnection:0").decode() - result = await schema.execute_async(query, variable_values=dict(after=after)) - assert not result.errors - expected = { - "allReporters": { - "edges": [ - {"node": {"firstName": "Jane", "lastName": "Roe"}}, - ] - } - } - assert result.data == expected - -@mark.asyncio -async def test_connection_should_succeed_if_last_higher_than_number_of_objects(): - class ReporterType(DjangoObjectType): - class Meta: - model = Reporter - interfaces = (Node,) - fields = "__all__" - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - schema = graphene.Schema(query=Query) - query = """ - query ReporterPromiseConnectionQuery ($last: Int) { - allReporters(last: $last) { - edges { - node { - firstName - lastName - } - } - } - } - """ - - result = await schema.execute_async(query, variable_values=dict(last=2)) - assert not result.errors - expected = {"allReporters": {"edges": []}} - assert result.data == expected - - Reporter.objects.create(first_name="John", last_name="Doe") - Reporter.objects.create(first_name="Some", last_name="Guy") - Reporter.objects.create(first_name="Jane", last_name="Roe") - Reporter.objects.create(first_name="Some", last_name="Lady") - - result = await schema.execute_async(query, variable_values=dict(last=2)) - assert not result.errors - expected = { - "allReporters": { - "edges": [ - {"node": {"firstName": "Jane", "lastName": "Roe"}}, - {"node": {"firstName": "Some", "lastName": "Lady"}}, - ] - } - } - assert result.data == expected - - result = await schema.execute_async(query, variable_values=dict(last=4)) - assert not result.errors - expected = { - "allReporters": { - "edges": [ - {"node": {"firstName": "John", "lastName": "Doe"}}, - {"node": {"firstName": "Some", "lastName": "Guy"}}, - {"node": {"firstName": "Jane", "lastName": "Roe"}}, - {"node": {"firstName": "Some", "lastName": "Lady"}}, - ] - } - } - assert result.data == expected - - result = await schema.execute_async(query, variable_values=dict(last=20)) - assert not result.errors - expected = { - "allReporters": { - "edges": [ - {"node": {"firstName": "John", "lastName": "Doe"}}, - {"node": {"firstName": "Some", "lastName": "Guy"}}, - {"node": {"firstName": "Jane", "lastName": "Roe"}}, - {"node": {"firstName": "Some", "lastName": "Lady"}}, - ] - } - } - assert result.data == expected - -@mark.asyncio -async def test_should_query_nullable_foreign_key(): - class PetType(DjangoObjectType): - class Meta: - model = Pet - - class PersonType(DjangoObjectType): - class Meta: - model = Person - - class Query(graphene.ObjectType): - pet = graphene.Field(PetType, name=graphene.String(required=True)) - person = graphene.Field(PersonType, name=graphene.String(required=True)) - - @staticmethod - @sync_to_async - def resolve_pet(self, info, name): - return Pet.objects.filter(name=name).first() - - @staticmethod - @sync_to_async - def resolve_person(self, info, name): - return Person.objects.filter(name=name).first() - - schema = graphene.Schema(query=Query) - - person = await Person.objects.acreate(name="Jane") - pets = [ - await Pet.objects.acreate(name="Stray dog", age=1), - await Pet.objects.acreate(name="Jane's dog", owner=person, age=1), - ] - - query_pet = """ - query getPet($name: String!) { - pet(name: $name) { - owner { - name - } - } - } - """ - result = await schema.execute_async(query_pet, variables={"name": "Stray dog"}) - assert not result.errors - assert result.data["pet"] == { - "owner": None, - } - - result = await schema.execute_async(query_pet, variables={"name": "Jane's dog"}) - assert not result.errors - assert result.data["pet"] == { - "owner": {"name": "Jane"}, - } - - query_owner = """ - query getOwner($name: String!) { - person(name: $name) { - pets { - name - } - } - } - """ - result = await schema.execute_async(query_owner, variables={"name": "Jane"}) - assert not result.errors - assert result.data["person"] == { - "pets": [{"name": "Jane's dog"}], - } From 4e5862f8fb0c8fb716a129abeb2cc6cdf278777d Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Thu, 4 May 2023 15:27:47 +0100 Subject: [PATCH 14/35] Add logging of errors in execution --- graphene_django/views.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/graphene_django/views.py b/graphene_django/views.py index a98a8c50d..f195efea2 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -1,6 +1,7 @@ import inspect import json import re +import traceback from asyncio import gather, coroutines @@ -519,7 +520,9 @@ async def dispatch(self, request, *args, **kwargs): ) if self.batch: - responses = await gather(*[self.get_response(request, entry) for entry in data]) + responses = await gather( + *[self.get_response(request, entry) for entry in data] + ) result = "[{}]".format( ",".join([response[0] for response in responses]) ) @@ -529,7 +532,9 @@ async def dispatch(self, request, *args, **kwargs): or 200 ) else: - result, status_code = await self.get_response(request, data, show_graphiql) + result, status_code = await self.get_response( + request, data, show_graphiql + ) return HttpResponse( status=status_code, content=result, content_type="application/json" @@ -558,6 +563,9 @@ async def get_response(self, request, data, show_graphiql=False): response = {} if execution_result.errors: + for e in execution_result.errors: + print(e) + traceback.print_tb(e.__traceback__) set_rollback() response["errors"] = [ self.format_error(e) for e in execution_result.errors From c10753d4b1d08b9f0f835e44e936567a9675aa2d Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Thu, 4 May 2023 15:51:25 +0100 Subject: [PATCH 15/35] most recent changes --- .pre-commit-config.yaml | 2 +- graphene_django/converter.py | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9214d35eb..adb54c7a1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.11 + python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 90c2128df..760973f09 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -1,6 +1,7 @@ from collections import OrderedDict from functools import singledispatch, wraps from asyncio import get_running_loop +from asgiref.sync import sync_to_async from django.db import models from django.utils.encoding import force_str @@ -265,17 +266,17 @@ def dynamic_type(): _type = registry.get_type_for_model(model) if not _type: return - + class CustomField(Field): def wrap_resolve(self, parent_resolver): resolver = super().wrap_resolve(parent_resolver) - try: + try: get_running_loop() except RuntimeError: pass else: - resolver=sync_to_async(resolver) + resolver = sync_to_async(resolver) return resolver @@ -334,7 +335,20 @@ def dynamic_type(): if not _type: return - return Field( + class CustomField(Field): + def wrap_resolve(self, parent_resolver): + resolver = super().wrap_resolve(parent_resolver) + + try: + get_running_loop() + except RuntimeError: + pass + else: + resolver = sync_to_async(resolver) + + return resolver + + return CustomField( _type, description=get_django_field_description(field), required=not field.null, From e9d5e88ea25b68c57c4a07a576010ea60f2dbfbe Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Fri, 5 May 2023 11:18:21 +0100 Subject: [PATCH 16/35] Handle the default django list field and test the async execution of the fields --- graphene_django/fields.py | 52 ++++++-- graphene_django/tests/async_test_helper.py | 6 + graphene_django/tests/test_fields.py | 139 +++++++++++++++++++++ 3 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 graphene_django/tests/async_test_helper.py diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 6e1b0b1de..99e84c76b 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -53,7 +53,28 @@ def get_manager(self): def list_resolver( django_object_type, resolver, default_manager, root, info, **args ): - queryset = maybe_queryset(resolver(root, info, **args)) + iterable = resolver(root, info, **args) + + if info.is_awaitable(iterable): + + async def resolve_list_async(iterable): + queryset = maybe_queryset(await iterable) + if queryset is None: + queryset = maybe_queryset(default_manager) + + if isinstance(queryset, QuerySet): + # Pass queryset to the DjangoObjectType get_queryset method + queryset = maybe_queryset( + await sync_to_async(django_object_type.get_queryset)( + queryset, info + ) + ) + + return await sync_to_async(list)(queryset) + + return resolve_list_async(iterable) + + queryset = maybe_queryset(iterable) if queryset is None: queryset = maybe_queryset(default_manager) @@ -61,12 +82,12 @@ def list_resolver( # Pass queryset to the DjangoObjectType get_queryset method queryset = maybe_queryset(django_object_type.get_queryset(queryset, info)) - try: + try: get_running_loop() except RuntimeError: - pass + pass else: - return queryset.aiterator() + return sync_to_async(list)(queryset) return queryset @@ -238,34 +259,39 @@ def connection_resolver( # or a resolve_foo (does not accept queryset) iterable = resolver(root, info, **args) - + if info.is_awaitable(iterable): + async def resolve_connection_async(iterable): iterable = await iterable if iterable is None: iterable = default_manager ## This could also be async iterable = queryset_resolver(connection, iterable, info, args) - + if info.is_awaitable(iterable): iterable = await iterable - - return await sync_to_async(cls.resolve_connection)(connection, args, iterable, max_limit=max_limit) + + return await sync_to_async(cls.resolve_connection)( + connection, args, iterable, max_limit=max_limit + ) + return resolve_connection_async(iterable) - + if iterable is None: iterable = default_manager # thus the iterable gets refiltered by resolve_queryset # but iterable might be promise iterable = queryset_resolver(connection, iterable, info, args) - try: + try: get_running_loop() except RuntimeError: - pass + pass else: - return sync_to_async(cls.resolve_connection)(connection, args, iterable, max_limit=max_limit) - + return sync_to_async(cls.resolve_connection)( + connection, args, iterable, max_limit=max_limit + ) return cls.resolve_connection(connection, args, iterable, max_limit=max_limit) diff --git a/graphene_django/tests/async_test_helper.py b/graphene_django/tests/async_test_helper.py new file mode 100644 index 000000000..0487f8918 --- /dev/null +++ b/graphene_django/tests/async_test_helper.py @@ -0,0 +1,6 @@ +from asgiref.sync import async_to_sync + + +def assert_async_result_equal(schema, query, result): + async_result = async_to_sync(schema.execute_async)(query) + assert async_result == result diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index 8c7b78d36..2a5055398 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -1,6 +1,7 @@ import datetime import re from django.db.models import Count, Prefetch +from asgiref.sync import sync_to_async, async_to_sync import pytest @@ -14,6 +15,7 @@ FilmDetails as FilmDetailsModel, Reporter as ReporterModel, ) +from .async_test_helper import assert_async_result_equal class TestDjangoListField: @@ -75,6 +77,7 @@ class Query(ObjectType): result = schema.execute(query) + assert_async_result_equal(schema, query, result) assert not result.errors assert result.data == { "reporters": [{"firstName": "Tara"}, {"firstName": "Debra"}] @@ -102,6 +105,7 @@ class Query(ObjectType): result = schema.execute(query) assert not result.errors assert result.data == {"reporters": []} + assert_async_result_equal(schema, query, result) ReporterModel.objects.create(first_name="Tara", last_name="West") ReporterModel.objects.create(first_name="Debra", last_name="Payne") @@ -112,6 +116,7 @@ class Query(ObjectType): assert result.data == { "reporters": [{"firstName": "Tara"}, {"firstName": "Debra"}] } + assert_async_result_equal(schema, query, result) def test_override_resolver(self): class Reporter(DjangoObjectType): @@ -139,6 +144,37 @@ def resolve_reporters(_, info): ReporterModel.objects.create(first_name="Debra", last_name="Payne") result = schema.execute(query) + assert not result.errors + assert result.data == {"reporters": [{"firstName": "Tara"}]} + + def test_override_resolver_async_execution(self): + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + fields = ("first_name",) + + class Query(ObjectType): + reporters = DjangoListField(Reporter) + + @staticmethod + @sync_to_async + def resolve_reporters(_, info): + return ReporterModel.objects.filter(first_name="Tara") + + schema = Schema(query=Query) + + query = """ + query { + reporters { + firstName + } + } + """ + + ReporterModel.objects.create(first_name="Tara", last_name="West") + ReporterModel.objects.create(first_name="Debra", last_name="Payne") + + result = async_to_sync(schema.execute_async)(query) assert not result.errors assert result.data == {"reporters": [{"firstName": "Tara"}]} @@ -203,6 +239,7 @@ class Query(ObjectType): {"firstName": "Debra", "articles": []}, ] } + assert_async_result_equal(schema, query, result) def test_override_resolver_nested_list_field(self): class Article(DjangoObjectType): @@ -261,6 +298,7 @@ class Query(ObjectType): {"firstName": "Debra", "articles": []}, ] } + assert_async_result_equal(schema, query, result) def test_get_queryset_filter(self): class Reporter(DjangoObjectType): @@ -306,6 +344,7 @@ def resolve_reporters(_, info): assert not result.errors assert result.data == {"reporters": [{"firstName": "Tara"}]} + assert_async_result_equal(schema, query, result) def test_resolve_list(self): """Resolving a plain list should work (and not call get_queryset)""" @@ -354,6 +393,55 @@ def resolve_reporters(_, info): assert not result.errors assert result.data == {"reporters": [{"firstName": "Debra"}]} + def test_resolve_list_async(self): + """Resolving a plain list should work (and not call get_queryset) when running under async""" + + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + fields = ("first_name", "articles") + + @classmethod + def get_queryset(cls, queryset, info): + # Only get reporters with at least 1 article + return queryset.annotate(article_count=Count("articles")).filter( + article_count__gt=0 + ) + + class Query(ObjectType): + reporters = DjangoListField(Reporter) + + @staticmethod + @sync_to_async + def resolve_reporters(_, info): + return [ReporterModel.objects.get(first_name="Debra")] + + schema = Schema(query=Query) + + query = """ + query { + reporters { + firstName + } + } + """ + + r1 = ReporterModel.objects.create(first_name="Tara", last_name="West") + ReporterModel.objects.create(first_name="Debra", last_name="Payne") + + ArticleModel.objects.create( + headline="Amazing news", + reporter=r1, + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + editor=r1, + ) + + result = async_to_sync(schema.execute_async)(query) + + assert not result.errors + assert result.data == {"reporters": [{"firstName": "Debra"}]} + def test_get_queryset_foreign_key(self): class Article(DjangoObjectType): class Meta: @@ -413,6 +501,7 @@ class Query(ObjectType): {"firstName": "Debra", "articles": []}, ] } + assert_async_result_equal(schema, query, result) def test_resolve_list_external_resolver(self): """Resolving a plain list from external resolver should work (and not call get_queryset)""" @@ -461,6 +550,54 @@ class Query(ObjectType): assert not result.errors assert result.data == {"reporters": [{"firstName": "Debra"}]} + def test_resolve_list_external_resolver_async(self): + """Resolving a plain list from external resolver should work (and not call get_queryset)""" + + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + fields = ("first_name", "articles") + + @classmethod + def get_queryset(cls, queryset, info): + # Only get reporters with at least 1 article + return queryset.annotate(article_count=Count("articles")).filter( + article_count__gt=0 + ) + + @sync_to_async + def resolve_reporters(_, info): + return [ReporterModel.objects.get(first_name="Debra")] + + class Query(ObjectType): + reporters = DjangoListField(Reporter, resolver=resolve_reporters) + + schema = Schema(query=Query) + + query = """ + query { + reporters { + firstName + } + } + """ + + r1 = ReporterModel.objects.create(first_name="Tara", last_name="West") + ReporterModel.objects.create(first_name="Debra", last_name="Payne") + + ArticleModel.objects.create( + headline="Amazing news", + reporter=r1, + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + editor=r1, + ) + + result = async_to_sync(schema.execute_async)(query) + + assert not result.errors + assert result.data == {"reporters": [{"firstName": "Debra"}]} + def test_get_queryset_filter_external_resolver(self): class Reporter(DjangoObjectType): class Meta: @@ -505,6 +642,7 @@ class Query(ObjectType): assert not result.errors assert result.data == {"reporters": [{"firstName": "Tara"}]} + assert_async_result_equal(schema, query, result) def test_select_related_and_prefetch_related_are_respected( self, django_assert_num_queries @@ -647,3 +785,4 @@ def resolve_articles(root, info): r'SELECT .* FROM "tests_film" INNER JOIN "tests_film_reporters" .* LEFT OUTER JOIN "tests_filmdetails"', captured.captured_queries[1]["sql"], ) + assert_async_result_equal(schema, query, result) From 76eeea4e78ba31e61201a95b9c16c1d4fb1a0546 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Fri, 5 May 2023 16:19:13 +0100 Subject: [PATCH 17/35] Update tests for queries --- graphene_django/tests/async_test_helper.py | 4 +- graphene_django/tests/test_query.py | 169 +++++++++++++++++++-- 2 files changed, 162 insertions(+), 11 deletions(-) diff --git a/graphene_django/tests/async_test_helper.py b/graphene_django/tests/async_test_helper.py index 0487f8918..5785c6c1e 100644 --- a/graphene_django/tests/async_test_helper.py +++ b/graphene_django/tests/async_test_helper.py @@ -1,6 +1,6 @@ from asgiref.sync import async_to_sync -def assert_async_result_equal(schema, query, result): - async_result = async_to_sync(schema.execute_async)(query) +def assert_async_result_equal(schema, query, result, **kwargs): + async_result = async_to_sync(schema.execute_async)(query, **kwargs) assert async_result == result diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 383ff2e33..19343afd9 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -7,6 +7,7 @@ from django.utils.functional import SimpleLazyObject from graphql_relay import to_global_id from pytest import raises +from asgiref.sync import sync_to_async, async_to_sync import graphene from graphene.relay import Node @@ -16,6 +17,7 @@ from ..types import DjangoObjectType from ..utils import DJANGO_FILTER_INSTALLED from .models import Article, CNNReporter, Film, FilmDetails, Person, Pet, Reporter +from .async_test_helper import assert_async_result_equal def test_should_query_only_fields(): @@ -34,6 +36,7 @@ class Meta: """ result = schema.execute(query) assert not result.errors + assert_async_result_equal(schema, query, result) def test_should_query_simplelazy_objects(): @@ -59,6 +62,7 @@ def resolve_reporter(self, info): result = schema.execute(query) assert not result.errors assert result.data == {"reporter": {"id": "1"}} + assert_async_result_equal(schema, query, result) def test_should_query_wrapped_simplelazy_objects(): @@ -84,6 +88,7 @@ def resolve_reporter(self, info): result = schema.execute(query) assert not result.errors assert result.data == {"reporter": {"id": "1"}} + assert_async_result_equal(schema, query, result) def test_should_query_well(): @@ -112,6 +117,7 @@ def resolve_reporter(self, info): result = schema.execute(query) assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result) @pytest.mark.skipif(IntegerRangeField is MissingType, reason="RangeField should exist") @@ -167,6 +173,7 @@ def resolve_event(self, info): result = schema.execute(query) assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result) def test_should_node(): @@ -248,6 +255,7 @@ def resolve_reporter(self, info): result = schema.execute(query) assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result) def test_should_query_onetoone_fields(): @@ -306,6 +314,7 @@ def resolve_film_details(root, info): result = schema.execute(query) assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result) def test_should_query_connectionfields(): @@ -344,6 +353,7 @@ def resolve_all_reporters(self, info, **args): "edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}], } } + assert_async_result_equal(schema, query, result) def test_should_keep_annotations(): @@ -403,6 +413,7 @@ def resolve_all_articles(self, info, **args): """ result = schema.execute(query) assert not result.errors + assert_async_result_equal(schema, query, result) @pytest.mark.skipif( @@ -484,6 +495,7 @@ class Query(graphene.ObjectType): result = schema.execute(query) assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result) @pytest.mark.skipif( @@ -529,6 +541,7 @@ def resolve_films(self, info, **args): result = schema.execute(query) assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result) @pytest.mark.skipif( @@ -618,6 +631,7 @@ class Query(graphene.ObjectType): result = schema.execute(query) assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result) def test_should_enforce_first_or_last(graphene_settings): @@ -658,6 +672,7 @@ class Query(graphene.ObjectType): "paginate the `allReporters` connection.\n" ) assert result.data == expected + assert_async_result_equal(schema, query, result) def test_should_error_if_first_is_greater_than_max(graphene_settings): @@ -700,6 +715,7 @@ class Query(graphene.ObjectType): "exceeds the `first` limit of 100 records.\n" ) assert result.data == expected + assert_async_result_equal(schema, query, result) def test_should_error_if_last_is_greater_than_max(graphene_settings): @@ -742,6 +758,7 @@ class Query(graphene.ObjectType): "exceeds the `last` limit of 100 records.\n" ) assert result.data == expected + assert_async_result_equal(schema, query, result) def test_should_query_promise_connectionfields(): @@ -777,6 +794,7 @@ def resolve_all_reporters(self, info, **args): result = schema.execute(query) assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result) def test_should_query_connectionfields_with_last(): @@ -814,6 +832,7 @@ def resolve_all_reporters(self, info, **args): result = schema.execute(query) assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result) def test_should_query_connectionfields_with_manager(): @@ -855,6 +874,7 @@ def resolve_all_reporters(self, info, **args): result = schema.execute(query) assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result) def test_should_query_dataloader_fields(): @@ -957,6 +977,108 @@ class Query(graphene.ObjectType): assert result.data == expected +def test_should_query_dataloader_fields_async(): + from promise import Promise + from promise.dataloader import DataLoader + + def article_batch_load_fn(keys): + queryset = Article.objects.filter(reporter_id__in=keys) + return Promise.resolve( + [ + [article for article in queryset if article.reporter_id == id] + for id in keys + ] + ) + + article_loader = DataLoader(article_batch_load_fn) + + class ArticleType(DjangoObjectType): + class Meta: + model = Article + interfaces = (Node,) + fields = "__all__" + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + use_connection = True + fields = "__all__" + + articles = DjangoConnectionField(ArticleType) + + @staticmethod + @sync_to_async + def resolve_articles(self, info, **args): + return article_loader.load(self.id).get() + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + + Article.objects.create( + headline="Article Node 1", + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + reporter=r, + editor=r, + lang="es", + ) + Article.objects.create( + headline="Article Node 2", + pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), + reporter=r, + editor=r, + lang="en", + ) + + schema = graphene.Schema(query=Query) + query = """ + query ReporterPromiseConnectionQuery { + allReporters(first: 1) { + edges { + node { + id + articles(first: 2) { + edges { + node { + headline + } + } + } + } + } + } + } + """ + + expected = { + "allReporters": { + "edges": [ + { + "node": { + "id": "UmVwb3J0ZXJUeXBlOjE=", + "articles": { + "edges": [ + {"node": {"headline": "Article Node 1"}}, + {"node": {"headline": "Article Node 2"}}, + ] + }, + } + } + ] + } + } + + result = async_to_sync(schema.execute_async)(query) + assert not result.errors + assert result.data == expected + + def test_should_handle_inherited_choices(): class BaseModel(models.Model): choice_field = models.IntegerField(choices=((0, "zero"), (1, "one"))) @@ -1063,6 +1185,7 @@ class Query(graphene.ObjectType): result = schema.execute(query) assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result) def test_should_resolve_get_queryset_connectionfields(): @@ -1108,6 +1231,7 @@ class Query(graphene.ObjectType): result = schema.execute(query) assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result) def test_connection_should_limit_after_to_list_length(): @@ -1145,6 +1269,7 @@ class Query(graphene.ObjectType): expected = {"allReporters": {"edges": []}} assert not result.errors assert result.data == expected + assert_async_result_equal(schema, query, result, variable_values=dict(after=after)) REPORTERS = [ @@ -1188,6 +1313,7 @@ class Query(graphene.ObjectType): result = schema.execute(query) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 4 + assert_async_result_equal(schema, query, result) def test_should_have_next_page(graphene_settings): @@ -1226,6 +1352,7 @@ class Query(graphene.ObjectType): assert not result.errors assert len(result.data["allReporters"]["edges"]) == 4 assert result.data["allReporters"]["pageInfo"]["hasNextPage"] + assert_async_result_equal(schema, query, result, variable_values={}) last_result = result.data["allReporters"]["pageInfo"]["endCursor"] result2 = schema.execute(query, variable_values=dict(first=4, after=last_result)) @@ -1239,6 +1366,9 @@ class Query(graphene.ObjectType): assert {to_global_id("ReporterType", reporter.id) for reporter in db_reporters} == { gql_reporter["node"]["id"] for gql_reporter in gql_reporters } + assert_async_result_equal( + schema, query, result2, variable_values=dict(first=4, after=last_result) + ) @pytest.mark.parametrize("max_limit", [100, 4]) @@ -1262,7 +1392,7 @@ class Query(graphene.ObjectType): def test_query_last(self, graphene_settings, max_limit): schema = self.setup_schema(graphene_settings, max_limit=max_limit) - query_last = """ + query = """ query { allReporters(last: 3) { edges { @@ -1274,16 +1404,17 @@ def test_query_last(self, graphene_settings, max_limit): } """ - result = schema.execute(query_last) + result = schema.execute(query) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 3 assert [ e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 3", "First 4", "First 5"] + assert_async_result_equal(schema, query, result) def test_query_first_and_last(self, graphene_settings, max_limit): schema = self.setup_schema(graphene_settings, max_limit=max_limit) - query_first_and_last = """ + query = """ query { allReporters(first: 4, last: 3) { edges { @@ -1295,16 +1426,17 @@ def test_query_first_and_last(self, graphene_settings, max_limit): } """ - result = schema.execute(query_first_and_last) + result = schema.execute(query) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 3 assert [ e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 1", "First 2", "First 3"] + assert_async_result_equal(schema, query, result) def test_query_first_last_and_after(self, graphene_settings, max_limit): schema = self.setup_schema(graphene_settings, max_limit=max_limit) - query_first_last_and_after = """ + query = """ query queryAfter($after: String) { allReporters(first: 4, last: 3, after: $after) { edges { @@ -1318,7 +1450,7 @@ def test_query_first_last_and_after(self, graphene_settings, max_limit): after = base64.b64encode(b"arrayconnection:0").decode() result = schema.execute( - query_first_last_and_after, + query, variable_values=dict(after=after), ) assert not result.errors @@ -1326,10 +1458,13 @@ def test_query_first_last_and_after(self, graphene_settings, max_limit): assert [ e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 2", "First 3", "First 4"] + assert_async_result_equal( + schema, query, result, variable_values=dict(after=after) + ) def test_query_last_and_before(self, graphene_settings, max_limit): schema = self.setup_schema(graphene_settings, max_limit=max_limit) - query_first_last_and_after = """ + query = """ query queryAfter($before: String) { allReporters(last: 1, before: $before) { edges { @@ -1342,20 +1477,24 @@ def test_query_last_and_before(self, graphene_settings, max_limit): """ result = schema.execute( - query_first_last_and_after, + query, ) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 1 assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 5" + assert_async_result_equal(schema, query, result) before = base64.b64encode(b"arrayconnection:5").decode() result = schema.execute( - query_first_last_and_after, + query, variable_values=dict(before=before), ) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 1 assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 4" + assert_async_result_equal( + schema, query, result, variable_values=dict(before=before) + ) def test_should_preserve_prefetch_related(django_assert_num_queries): @@ -1410,6 +1549,7 @@ def resolve_films(root, info, **kwargs): with django_assert_num_queries(3) as captured: result = schema.execute(query) assert not result.errors + assert_async_result_equal(schema, query, result) def test_should_preserve_annotations(): @@ -1465,6 +1605,7 @@ def resolve_films(root, info, **kwargs): } assert result.data == expected, str(result.data) assert not result.errors + assert_async_result_equal(schema, query, result) def test_connection_should_enable_offset_filtering(): @@ -1504,6 +1645,7 @@ class Query(graphene.ObjectType): } } assert result.data == expected + assert_async_result_equal(schema, query, result) def test_connection_should_enable_offset_filtering_higher_than_max_limit( @@ -1548,6 +1690,7 @@ class Query(graphene.ObjectType): } } assert result.data == expected + assert_async_result_equal(schema, query, result) def test_connection_should_forbid_offset_filtering_with_before(): @@ -1578,6 +1721,9 @@ class Query(graphene.ObjectType): expected_error = "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `allReporters` connection." assert len(result.errors) == 1 assert result.errors[0].message == expected_error + assert_async_result_equal( + schema, query, result, variable_values=dict(before=before) + ) def test_connection_should_allow_offset_filtering_with_after(): @@ -1620,6 +1766,7 @@ class Query(graphene.ObjectType): } } assert result.data == expected + assert_async_result_equal(schema, query, result, variable_values=dict(after=after)) def test_connection_should_succeed_if_last_higher_than_number_of_objects(): @@ -1650,6 +1797,7 @@ class Query(graphene.ObjectType): assert not result.errors expected = {"allReporters": {"edges": []}} assert result.data == expected + assert_async_result_equal(schema, query, result, variable_values=dict(last=2)) Reporter.objects.create(first_name="John", last_name="Doe") Reporter.objects.create(first_name="Some", last_name="Guy") @@ -1667,6 +1815,7 @@ class Query(graphene.ObjectType): } } assert result.data == expected + assert_async_result_equal(schema, query, result, variable_values=dict(last=2)) result = schema.execute(query, variable_values=dict(last=4)) assert not result.errors @@ -1681,6 +1830,7 @@ class Query(graphene.ObjectType): } } assert result.data == expected + assert_async_result_equal(schema, query, result, variable_values=dict(last=4)) result = schema.execute(query, variable_values=dict(last=20)) assert not result.errors @@ -1695,6 +1845,7 @@ class Query(graphene.ObjectType): } } assert result.data == expected + assert_async_result_equal(schema, query, result, variable_values=dict(last=20)) def test_should_query_nullable_foreign_key(): From c501fdb20b60ec328033639f11b2fc7daaa57990 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Fri, 5 May 2023 16:25:33 +0100 Subject: [PATCH 18/35] swap back to python 3.11 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index adb54c7a1..9214d35eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.10 + python: python3.11 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 From 58b92e6ed3e505b57a46162d42b23b2ea2d56353 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Fri, 5 May 2023 16:28:40 +0100 Subject: [PATCH 19/35] linting --- graphene_django/filter/fields.py | 2 ++ graphene_django/rest_framework/mutation.py | 6 ++++-- graphene_django/types.py | 5 +++-- setup.py | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index c62ee9ca9..a4ae821ba 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -95,6 +95,7 @@ def filter_kwargs(): qs = super().resolve_queryset(connection, iterable, info, args) if info.is_awaitable(qs): + async def filter_async(qs): filterset = filterset_class( data=filter_kwargs(), queryset=await qs, request=info.context @@ -102,6 +103,7 @@ async def filter_async(qs): if await sync_to_async(filterset.is_valid)(): return filterset.qs raise ValidationError(filterset.form.errors.as_json()) + return filter_async(qs) filterset = filterset_class( diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index 8cdd4701c..46d3593c1 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -154,17 +154,19 @@ def mutate_and_get_payload(cls, root, info, **input): kwargs = cls.get_serializer_kwargs(root, info, **input) serializer = cls._meta.serializer_class(**kwargs) - try: + try: get_running_loop() except RuntimeError: - pass + pass else: + async def perform_mutate_async(): if await sync_to_async(serializer.is_valid)(): return await sync_to_async(cls.perform_mutate)(serializer, info) else: errors = ErrorType.from_errors(serializer.errors) return cls(errors=errors) + return perform_mutate_async() if serializer.is_valid(): diff --git a/graphene_django/types.py b/graphene_django/types.py index 068d2680d..a064b9046 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -288,14 +288,15 @@ def get_queryset(cls, queryset, info): def get_node(cls, info, id): queryset = cls.get_queryset(cls._meta.model.objects, info) try: - try: + try: import asyncio + asyncio.get_running_loop() except RuntimeError: pass else: return queryset.aget(pk=id) - + return queryset.get(pk=id) except cls._meta.model.DoesNotExist: return None diff --git a/setup.py b/setup.py index 64273e2a8..56a13b412 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ "pytz", "django-filter>=22.1", "pytest-django>=4.5.2", - "pytest-asyncio>=0.16,<2" + "pytest-asyncio>=0.16,<2", ] + rest_framework_require From 791209f5574719962b102cdf32399262fd2f3cb3 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Tue, 9 May 2023 21:52:55 +0100 Subject: [PATCH 20/35] Rejig concept to use middleware --- graphene_django/converter.py | 32 ++--------------------------- graphene_django/debug/middleware.py | 27 ++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 760973f09..375d68312 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -1,7 +1,5 @@ from collections import OrderedDict from functools import singledispatch, wraps -from asyncio import get_running_loop -from asgiref.sync import sync_to_async from django.db import models from django.utils.encoding import force_str @@ -267,20 +265,7 @@ def dynamic_type(): if not _type: return - class CustomField(Field): - def wrap_resolve(self, parent_resolver): - resolver = super().wrap_resolve(parent_resolver) - - try: - get_running_loop() - except RuntimeError: - pass - else: - resolver = sync_to_async(resolver) - - return resolver - - return CustomField(_type, required=not field.null) + return Field(_type, required=not field.null) return Dynamic(dynamic_type) @@ -335,20 +320,7 @@ def dynamic_type(): if not _type: return - class CustomField(Field): - def wrap_resolve(self, parent_resolver): - resolver = super().wrap_resolve(parent_resolver) - - try: - get_running_loop() - except RuntimeError: - pass - else: - resolver = sync_to_async(resolver) - - return resolver - - return CustomField( + return Field( _type, description=get_django_field_description(field), required=not field.null, diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index d3052a14a..40a951a98 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -1,7 +1,7 @@ from django.db import connections -from promise import Promise - +from asgiref.sync import sync_to_async +import inspect from .sql.tracking import unwrap_cursor, wrap_cursor from .exception.formating import wrap_exception from .types import DjangoDebug @@ -69,3 +69,26 @@ def resolve(self, next, root, info, **args): return context.django_debug.on_resolve_error(e) context.django_debug.add_result(result) return result + + +class DjangoSyncRequiredMiddleware: + def resolve(self, next, root, info, **args): + parent_type = info.parent_type + + ## Anytime the parent is a DjangoObject type + # and we're resolving a sync field, we need to wrap it in a sync_to_async + if hasattr(parent_type, "graphene_type") and hasattr( + parent_type.graphene_type._meta, "model" + ): + if not inspect.iscoroutinefunction(next): + return sync_to_async(next)(root, info, **args) + + ## In addition, if we're resolving to a DjangoObject type + # we likely need to wrap it in a sync_to_async as well + if hasattr(info.return_type, "graphene_type") and hasattr( + info.return_type.graphene_type._meta, "model" + ): + if not info.is_awaitable(next): + return sync_to_async(next)(root, info, **args) + + return next(root, info, **args) From b69476f50f0442c892a826329317f787a144b20b Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Tue, 9 May 2023 22:58:15 +0100 Subject: [PATCH 21/35] improve async detection --- graphene_django/debug/middleware.py | 8 ++++++-- graphene_django/fields.py | 7 +++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index 40a951a98..74366ddcc 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -80,7 +80,9 @@ def resolve(self, next, root, info, **args): if hasattr(parent_type, "graphene_type") and hasattr( parent_type.graphene_type._meta, "model" ): - if not inspect.iscoroutinefunction(next): + if not inspect.iscoroutinefunction(next) and not inspect.isasyncgenfunction( + next + ): return sync_to_async(next)(root, info, **args) ## In addition, if we're resolving to a DjangoObject type @@ -88,7 +90,9 @@ def resolve(self, next, root, info, **args): if hasattr(info.return_type, "graphene_type") and hasattr( info.return_type.graphene_type._meta, "model" ): - if not info.is_awaitable(next): + if not inspect.iscoroutinefunction(next) and not inspect.isasyncgenfunction( + next + ): return sync_to_async(next)(root, info, **args) return next(root, info, **args) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 99e84c76b..43225c293 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -266,11 +266,10 @@ async def resolve_connection_async(iterable): iterable = await iterable if iterable is None: iterable = default_manager - ## This could also be async - iterable = queryset_resolver(connection, iterable, info, args) - if info.is_awaitable(iterable): - iterable = await iterable + iterable = await sync_to_async(queryset_resolver)( + connection, iterable, info, args + ) return await sync_to_async(cls.resolve_connection)( connection, args, iterable, max_limit=max_limit From b134ab0a3e74aa35436aa25f62bcee0dcfbfa35b Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Tue, 9 May 2023 23:27:02 +0100 Subject: [PATCH 22/35] Handle custom Djangoconnectionresolvers --- graphene_django/debug/middleware.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index 74366ddcc..c0a316b6e 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -95,4 +95,15 @@ def resolve(self, next, root, info, **args): ): return sync_to_async(next)(root, info, **args) + ## We also need to handle custom resolvers around Connections + # but only when their parent is not already a DjangoObject type + # this case already gets handled above. + if hasattr(info.return_type, "graphene_type"): + if hasattr(info.return_type.graphene_type, "Edge"): + node_type = info.return_type.graphene_type.Edge.node.type + if hasattr(node_type, "_meta") and hasattr(node_type._meta, "model"): + if not inspect.iscoroutinefunction( + next + ) and not inspect.isasyncgenfunction(next): + return sync_to_async(next)(root, info, **args) return next(root, info, **args) From 8c068fbc2b9a5b388022405af21b701a29ecc3fc Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Wed, 10 May 2023 19:17:30 +0100 Subject: [PATCH 23/35] Update to pull out mutations and ensure that DjangoFields don't get double sync'd --- graphene_django/debug/middleware.py | 30 ++++++++++++++++------ graphene_django/fields.py | 40 +++++++++++++++++++---------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index c0a316b6e..f8ea42ec3 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -5,6 +5,9 @@ from .sql.tracking import unwrap_cursor, wrap_cursor from .exception.formating import wrap_exception from .types import DjangoDebug +from graphql.type.definition import GraphQLNonNull + +from django.db.models import QuerySet class DjangoDebugContext: @@ -74,6 +77,12 @@ def resolve(self, next, root, info, **args): class DjangoSyncRequiredMiddleware: def resolve(self, next, root, info, **args): parent_type = info.parent_type + return_type = info.return_type + + if isinstance(parent_type, GraphQLNonNull): + parent_type = parent_type.of_type + if isinstance(return_type, GraphQLNonNull): + return_type = return_type.of_type ## Anytime the parent is a DjangoObject type # and we're resolving a sync field, we need to wrap it in a sync_to_async @@ -87,23 +96,28 @@ def resolve(self, next, root, info, **args): ## In addition, if we're resolving to a DjangoObject type # we likely need to wrap it in a sync_to_async as well - if hasattr(info.return_type, "graphene_type") and hasattr( - info.return_type.graphene_type._meta, "model" + if hasattr(return_type, "graphene_type") and hasattr( + return_type.graphene_type._meta, "model" ): if not inspect.iscoroutinefunction(next) and not inspect.isasyncgenfunction( next ): return sync_to_async(next)(root, info, **args) - ## We also need to handle custom resolvers around Connections - # but only when their parent is not already a DjangoObject type - # this case already gets handled above. - if hasattr(info.return_type, "graphene_type"): - if hasattr(info.return_type.graphene_type, "Edge"): - node_type = info.return_type.graphene_type.Edge.node.type + ## We can move this resolver logic into the field resolver itself and probably should + if hasattr(return_type, "graphene_type"): + if hasattr(return_type.graphene_type, "Edge"): + node_type = return_type.graphene_type.Edge.node.type if hasattr(node_type, "_meta") and hasattr(node_type._meta, "model"): if not inspect.iscoroutinefunction( next ) and not inspect.isasyncgenfunction(next): return sync_to_async(next)(root, info, **args) + + if info.parent_type.name == "Mutation": + if not inspect.iscoroutinefunction(next) and not inspect.isasyncgenfunction( + next + ): + return sync_to_async(next)(root, info, **args) + return next(root, info, **args) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 43225c293..8f4ce5f47 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,3 +1,4 @@ +import inspect from functools import partial from django.db.models.query import QuerySet @@ -82,13 +83,6 @@ async def resolve_list_async(iterable): # Pass queryset to the DjangoObjectType get_queryset method queryset = maybe_queryset(django_object_type.get_queryset(queryset, info)) - try: - get_running_loop() - except RuntimeError: - pass - else: - return sync_to_async(list)(queryset) - return queryset def wrap_resolve(self, parent_resolver): @@ -97,12 +91,31 @@ def wrap_resolve(self, parent_resolver): if isinstance(_type, NonNull): _type = _type.of_type django_object_type = _type.of_type.of_type - return partial( - self.list_resolver, - django_object_type, - resolver, - self.get_manager(), - ) + + try: + get_running_loop() + except RuntimeError: + return partial( + self.list_resolver, django_object_type, resolver, self.get_manager() + ) + else: + if not inspect.iscoroutinefunction( + resolver + ) and not inspect.isasyncgenfunction(resolver): + async_resolver = sync_to_async(resolver) + + ## This is needed because our middleware can't detect the resolver as async when we returns partial[couroutine] + async def wrapped_resolver(root, info, **args): + return await self.list_resolver( + django_object_type, + async_resolver, + self.get_manager(), + root, + info, + **args + ) + + return wrapped_resolver class DjangoConnectionField(ConnectionField): @@ -257,7 +270,6 @@ def connection_resolver( # eventually leads to DjangoObjectType's get_queryset (accepts queryset) # or a resolve_foo (does not accept queryset) - iterable = resolver(root, info, **args) if info.is_awaitable(iterable): From d3f8fcf906818add9fb849e5f5467ddf42000fa1 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Tue, 16 May 2023 16:07:49 +0100 Subject: [PATCH 24/35] handle connections without middleware --- graphene_django/debug/middleware.py | 10 ---------- graphene_django/fields.py | 26 ++++++++++++++++++++++---- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index f8ea42ec3..50b0cfea9 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -104,16 +104,6 @@ def resolve(self, next, root, info, **args): ): return sync_to_async(next)(root, info, **args) - ## We can move this resolver logic into the field resolver itself and probably should - if hasattr(return_type, "graphene_type"): - if hasattr(return_type.graphene_type, "Edge"): - node_type = return_type.graphene_type.Edge.node.type - if hasattr(node_type, "_meta") and hasattr(node_type._meta, "model"): - if not inspect.iscoroutinefunction( - next - ) and not inspect.isasyncgenfunction(next): - return sync_to_async(next)(root, info, **args) - if info.parent_type.name == "Mutation": if not inspect.iscoroutinefunction(next) and not inspect.isasyncgenfunction( next diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 8f4ce5f47..081d87012 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -270,6 +270,17 @@ def connection_resolver( # eventually leads to DjangoObjectType's get_queryset (accepts queryset) # or a resolve_foo (does not accept queryset) + + try: + get_running_loop() + except RuntimeError: + pass + else: + if not inspect.iscoroutinefunction( + resolver + ) and not inspect.isasyncgenfunction(resolver): + resolver = sync_to_async(resolver) + iterable = resolver(root, info, **args) if info.is_awaitable(iterable): @@ -293,17 +304,24 @@ async def resolve_connection_async(iterable): iterable = default_manager # thus the iterable gets refiltered by resolve_queryset # but iterable might be promise - iterable = queryset_resolver(connection, iterable, info, args) try: get_running_loop() except RuntimeError: pass else: - return sync_to_async(cls.resolve_connection)( - connection, args, iterable, max_limit=max_limit - ) + async def perform_resolve(iterable): + iterable = await sync_to_async(queryset_resolver)( + connection, iterable, info, args + ) + return await sync_to_async(cls.resolve_connection)( + connection, args, iterable, max_limit=max_limit + ) + + return perform_resolve(iterable) + + iterable = queryset_resolver(connection, iterable, info, args) return cls.resolve_connection(connection, args, iterable, max_limit=max_limit) def wrap_resolve(self, parent_resolver): From 930248f78d15cb7dc7dd8ba0e45a811de3359d77 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Tue, 16 May 2023 16:20:13 +0100 Subject: [PATCH 25/35] Refactor out async helper functions --- graphene_django/debug/middleware.py | 14 ++++------- graphene_django/fields.py | 27 +++++----------------- graphene_django/rest_framework/mutation.py | 8 ++----- graphene_django/types.py | 9 ++------ graphene_django/utils/__init__.py | 4 ++++ graphene_django/utils/utils.py | 16 +++++++++++++ 6 files changed, 35 insertions(+), 43 deletions(-) diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index 50b0cfea9..3e66a8f08 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -9,6 +9,8 @@ from django.db.models import QuerySet +from ..utils import is_sync_function + class DjangoDebugContext: def __init__(self): @@ -89,9 +91,7 @@ def resolve(self, next, root, info, **args): if hasattr(parent_type, "graphene_type") and hasattr( parent_type.graphene_type._meta, "model" ): - if not inspect.iscoroutinefunction(next) and not inspect.isasyncgenfunction( - next - ): + if is_sync_function(next): return sync_to_async(next)(root, info, **args) ## In addition, if we're resolving to a DjangoObject type @@ -99,15 +99,11 @@ def resolve(self, next, root, info, **args): if hasattr(return_type, "graphene_type") and hasattr( return_type.graphene_type._meta, "model" ): - if not inspect.iscoroutinefunction(next) and not inspect.isasyncgenfunction( - next - ): + if is_sync_function(next): return sync_to_async(next)(root, info, **args) if info.parent_type.name == "Mutation": - if not inspect.iscoroutinefunction(next) and not inspect.isasyncgenfunction( - next - ): + if is_sync_function(next): return sync_to_async(next)(root, info, **args) return next(root, info, **args) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 081d87012..db846b9da 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -11,7 +11,6 @@ ) from asgiref.sync import sync_to_async -from asyncio import get_running_loop from graphene import Int, NonNull from graphene.relay import ConnectionField @@ -19,7 +18,7 @@ from graphene.types import Field, List from .settings import graphene_settings -from .utils import maybe_queryset +from .utils import maybe_queryset, is_sync_function, is_running_async class DjangoListField(Field): @@ -92,16 +91,12 @@ def wrap_resolve(self, parent_resolver): _type = _type.of_type django_object_type = _type.of_type.of_type - try: - get_running_loop() - except RuntimeError: + if not is_running_async(): return partial( self.list_resolver, django_object_type, resolver, self.get_manager() ) else: - if not inspect.iscoroutinefunction( - resolver - ) and not inspect.isasyncgenfunction(resolver): + if is_sync_function(resolver): async_resolver = sync_to_async(resolver) ## This is needed because our middleware can't detect the resolver as async when we returns partial[couroutine] @@ -271,14 +266,8 @@ def connection_resolver( # eventually leads to DjangoObjectType's get_queryset (accepts queryset) # or a resolve_foo (does not accept queryset) - try: - get_running_loop() - except RuntimeError: - pass - else: - if not inspect.iscoroutinefunction( - resolver - ) and not inspect.isasyncgenfunction(resolver): + if is_running_async(): + if is_sync_function(resolver): resolver = sync_to_async(resolver) iterable = resolver(root, info, **args) @@ -305,11 +294,7 @@ async def resolve_connection_async(iterable): # thus the iterable gets refiltered by resolve_queryset # but iterable might be promise - try: - get_running_loop() - except RuntimeError: - pass - else: + if is_running_async(): async def perform_resolve(iterable): iterable = await sync_to_async(queryset_resolver)( diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index 46d3593c1..ca2764d0b 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -2,7 +2,6 @@ from django.shortcuts import get_object_or_404 from rest_framework import serializers -from asyncio import get_running_loop from asgiref.sync import sync_to_async import graphene @@ -13,6 +12,7 @@ from ..types import ErrorType from .serializer_converter import convert_serializer_field +from ..utils import is_running_async class SerializerMutationOptions(MutationOptions): @@ -154,11 +154,7 @@ def mutate_and_get_payload(cls, root, info, **input): kwargs = cls.get_serializer_kwargs(root, info, **input) serializer = cls._meta.serializer_class(**kwargs) - try: - get_running_loop() - except RuntimeError: - pass - else: + if is_running_async(): async def perform_mutate_async(): if await sync_to_async(serializer.is_valid)(): diff --git a/graphene_django/types.py b/graphene_django/types.py index a064b9046..7216cdee3 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -16,6 +16,7 @@ camelize, get_model_fields, is_valid_django_model, + is_running_async, ) ALL_FIELDS = "__all__" @@ -288,13 +289,7 @@ def get_queryset(cls, queryset, info): def get_node(cls, info, id): queryset = cls.get_queryset(cls._meta.model.objects, info) try: - try: - import asyncio - - asyncio.get_running_loop() - except RuntimeError: - pass - else: + if is_running_async(): return queryset.aget(pk=id) return queryset.get(pk=id) diff --git a/graphene_django/utils/__init__.py b/graphene_django/utils/__init__.py index 671b0609a..7344ce59a 100644 --- a/graphene_django/utils/__init__.py +++ b/graphene_django/utils/__init__.py @@ -6,6 +6,8 @@ get_reverse_fields, is_valid_django_model, maybe_queryset, + is_sync_function, + is_running_async, ) __all__ = [ @@ -16,4 +18,6 @@ "camelize", "is_valid_django_model", "GraphQLTestCase", + "is_sync_function", + "is_running_async", ] diff --git a/graphene_django/utils/utils.py b/graphene_django/utils/utils.py index 343a3a74c..beac3e61a 100644 --- a/graphene_django/utils/utils.py +++ b/graphene_django/utils/utils.py @@ -1,4 +1,5 @@ import inspect +from asyncio import get_running_loop from django.db import connection, models, transaction from django.db.models.manager import Manager @@ -105,3 +106,18 @@ def set_rollback(): atomic_requests = connection.settings_dict.get("ATOMIC_REQUESTS", False) if atomic_requests and connection.in_atomic_block: transaction.set_rollback(True) + + +def is_running_async(): + try: + get_running_loop() + except RuntimeError: + return False + else: + return True + + +def is_sync_function(func): + return not inspect.iscoroutinefunction(func) and not inspect.isasyncgenfunction( + func + ) From c27dd6c732d5e1b923a67763ede6af5eef02a10c Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Tue, 16 May 2023 17:03:24 +0100 Subject: [PATCH 26/35] cleanup middleware --- graphene_django/debug/middleware.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index 3e66a8f08..1e7bcf600 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -86,24 +86,16 @@ def resolve(self, next, root, info, **args): if isinstance(return_type, GraphQLNonNull): return_type = return_type.of_type - ## Anytime the parent is a DjangoObject type - # and we're resolving a sync field, we need to wrap it in a sync_to_async - if hasattr(parent_type, "graphene_type") and hasattr( - parent_type.graphene_type._meta, "model" + if any( + [ + hasattr(parent_type, "graphene_type") + and hasattr(parent_type.graphene_type._meta, "model"), + hasattr(return_type, "graphene_type") + and hasattr(return_type.graphene_type._meta, "model"), + info.parent_type.name == "Mutation", + ] ): if is_sync_function(next): return sync_to_async(next)(root, info, **args) - ## In addition, if we're resolving to a DjangoObject type - # we likely need to wrap it in a sync_to_async as well - if hasattr(return_type, "graphene_type") and hasattr( - return_type.graphene_type._meta, "model" - ): - if is_sync_function(next): - return sync_to_async(next)(root, info, **args) - - if info.parent_type.name == "Mutation": - if is_sync_function(next): - return sync_to_async(next)(root, info, **args) - return next(root, info, **args) From fba274dc4dd28993655fbf770a0dfcb0f2cb7b1b Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Tue, 16 May 2023 17:09:15 +0100 Subject: [PATCH 27/35] refactor middleware --- graphene_django/types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphene_django/types.py b/graphene_django/types.py index 7216cdee3..2ce9db309 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -293,6 +293,7 @@ def get_node(cls, info, id): return queryset.aget(pk=id) return queryset.get(pk=id) + except cls._meta.model.DoesNotExist: return None From 84ba7d775a7356bc62e0ce5eec9190d3f163c056 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Tue, 16 May 2023 17:46:23 +0100 Subject: [PATCH 28/35] Remove unused path --- graphene_django/fields.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index db846b9da..ce50d0b1d 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -293,19 +293,6 @@ async def resolve_connection_async(iterable): iterable = default_manager # thus the iterable gets refiltered by resolve_queryset # but iterable might be promise - - if is_running_async(): - - async def perform_resolve(iterable): - iterable = await sync_to_async(queryset_resolver)( - connection, iterable, info, args - ) - return await sync_to_async(cls.resolve_connection)( - connection, args, iterable, max_limit=max_limit - ) - - return perform_resolve(iterable) - iterable = queryset_resolver(connection, iterable, info, args) return cls.resolve_connection(connection, args, iterable, max_limit=max_limit) From 848536ee3a8dc7a9eb13e66646375d447a2bff58 Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Tue, 16 May 2023 19:45:02 +0100 Subject: [PATCH 29/35] Clean up wrapped list resolver --- graphene_django/fields.py | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index ce50d0b1d..f58082b48 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -49,10 +49,14 @@ def model(self): def get_manager(self): return self.model._default_manager - @staticmethod + @classmethod def list_resolver( - django_object_type, resolver, default_manager, root, info, **args + cls, django_object_type, resolver, default_manager, root, info, **args ): + if is_running_async(): + if is_sync_function(resolver): + resolver = sync_to_async(resolver) + iterable = resolver(root, info, **args) if info.is_awaitable(iterable): @@ -82,7 +86,7 @@ async def resolve_list_async(iterable): # Pass queryset to the DjangoObjectType get_queryset method queryset = maybe_queryset(django_object_type.get_queryset(queryset, info)) - return queryset + return list(queryset) def wrap_resolve(self, parent_resolver): resolver = super().wrap_resolve(parent_resolver) @@ -90,27 +94,12 @@ def wrap_resolve(self, parent_resolver): if isinstance(_type, NonNull): _type = _type.of_type django_object_type = _type.of_type.of_type - - if not is_running_async(): - return partial( - self.list_resolver, django_object_type, resolver, self.get_manager() - ) - else: - if is_sync_function(resolver): - async_resolver = sync_to_async(resolver) - - ## This is needed because our middleware can't detect the resolver as async when we returns partial[couroutine] - async def wrapped_resolver(root, info, **args): - return await self.list_resolver( - django_object_type, - async_resolver, - self.get_manager(), - root, - info, - **args - ) - - return wrapped_resolver + return partial( + self.list_resolver, + django_object_type, + resolver, + self.get_manager(), + ) class DjangoConnectionField(ConnectionField): From 2659d67fd08c648f6ea31fbc0a9fefc47565a3bf Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Tue, 16 May 2023 20:19:35 +0100 Subject: [PATCH 30/35] updates tests --- graphene_django/tests/test_fields.py | 5 ----- graphene_django/tests/test_query.py | 2 -- 2 files changed, 7 deletions(-) diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index 2a5055398..0ec6e82fd 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -156,8 +156,6 @@ class Meta: class Query(ObjectType): reporters = DjangoListField(Reporter) - @staticmethod - @sync_to_async def resolve_reporters(_, info): return ReporterModel.objects.filter(first_name="Tara") @@ -411,8 +409,6 @@ def get_queryset(cls, queryset, info): class Query(ObjectType): reporters = DjangoListField(Reporter) - @staticmethod - @sync_to_async def resolve_reporters(_, info): return [ReporterModel.objects.get(first_name="Debra")] @@ -565,7 +561,6 @@ def get_queryset(cls, queryset, info): article_count__gt=0 ) - @sync_to_async def resolve_reporters(_, info): return [ReporterModel.objects.get(first_name="Debra")] diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 19343afd9..951ca79cb 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1007,8 +1007,6 @@ class Meta: articles = DjangoConnectionField(ArticleType) - @staticmethod - @sync_to_async def resolve_articles(self, info, **args): return article_loader.load(self.id).get() From 37ebd6309b65d05067220b402d583f815cf98e2e Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Tue, 16 May 2023 20:26:05 +0100 Subject: [PATCH 31/35] follow main removing validate call --- graphene_django/views.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/graphene_django/views.py b/graphene_django/views.py index 7933c64c9..7386e6d5b 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -665,10 +665,6 @@ async def execute_graphql_request( ) ) - validation_errors = validate(self.schema.graphql_schema, document) - if validation_errors: - return ExecutionResult(data=None, errors=validation_errors) - try: extra_options = {} if self.execution_context_class: From 5a1911118247f5704e8d0e5e2060039c6cc65c80 Mon Sep 17 00:00:00 2001 From: Paul Craciunoiu Date: Thu, 25 May 2023 11:44:39 -0600 Subject: [PATCH 32/35] Fix graphiql for async --- graphene_django/views.py | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/graphene_django/views.py b/graphene_django/views.py index 7386e6d5b..8cdc1d118 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -403,28 +403,6 @@ def get_content_type(request): class AsyncGraphQLView(GraphQLView): - graphiql_template = "graphene/graphiql.html" - - # Polyfill for window.fetch. - whatwg_fetch_version = "3.6.2" - whatwg_fetch_sri = "sha256-+pQdxwAcHJdQ3e/9S4RK6g8ZkwdMgFQuHvLuN5uyk5c=" - - # React and ReactDOM. - react_version = "17.0.2" - react_sri = "sha256-Ipu/TQ50iCCVZBUsZyNJfxrDk0E2yhaEIz0vqI+kFG8=" - react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=" - - # The GraphiQL React app. - graphiql_version = "1.4.7" # "1.0.3" - graphiql_sri = "sha256-cpZ8w9D/i6XdEbY/Eu7yAXeYzReVw0mxYd7OU3gUcsc=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk=" - graphiql_css_sri = "sha256-HADQowUuFum02+Ckkv5Yu5ygRoLllHZqg0TFZXY7NHI=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E=" - - # The websocket transport library for subscriptions. - subscriptions_transport_ws_version = "0.9.18" - subscriptions_transport_ws_sri = ( - "sha256-i0hAXd4PdJ/cHX3/8tIy/Q/qKiWr5WSTxMFuL9tACkw=" - ) - schema = None graphiql = False middleware = None @@ -512,6 +490,8 @@ async def dispatch(self, request, *args, **kwargs): graphiql_css_sri=self.graphiql_css_sri, subscriptions_transport_ws_version=self.subscriptions_transport_ws_version, subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri, + graphiql_plugin_explorer_version=self.graphiql_plugin_explorer_version, + graphiql_plugin_explorer_sri=self.graphiql_plugin_explorer_sri, # The SUBSCRIPTION_PATH setting. subscription_path=self.subscription_path, # GraphiQL headers tab, From 28bc8584440fd17cc3cc8741a1038ff10b6be00b Mon Sep 17 00:00:00 2001 From: Firas Kafri Date: Thu, 10 Aug 2023 09:53:04 +0300 Subject: [PATCH 33/35] resolve conflicts and fix format --- examples/cookbook/cookbook/recipes/schema.py | 3 +- examples/cookbook/cookbook/urls.py | 4 +-- graphene_django/debug/middleware.py | 12 +++---- graphene_django/fields.py | 7 ++-- graphene_django/filter/fields.py | 3 +- graphene_django/rest_framework/mutation.py | 4 +-- graphene_django/tests/test_fields.py | 6 ++-- graphene_django/tests/test_query.py | 36 +++++++++++--------- graphene_django/types.py | 2 +- graphene_django/utils/__init__.py | 4 +-- graphene_django/utils/utils.py | 1 + graphene_django/views.py | 6 ++-- 12 files changed, 40 insertions(+), 48 deletions(-) diff --git a/examples/cookbook/cookbook/recipes/schema.py b/examples/cookbook/cookbook/recipes/schema.py index 19fbcc6eb..2f2fc032e 100644 --- a/examples/cookbook/cookbook/recipes/schema.py +++ b/examples/cookbook/cookbook/recipes/schema.py @@ -2,8 +2,7 @@ from asgiref.sync import sync_to_async -from cookbook.recipes.models import Recipe, RecipeIngredient -from graphene import Node, String, Field +from graphene import Field, Node, String from graphene_django.filter import DjangoFilterConnectionField from graphene_django.types import DjangoObjectType diff --git a/examples/cookbook/cookbook/urls.py b/examples/cookbook/cookbook/urls.py index 57687daa6..c0f6fdf65 100644 --- a/examples/cookbook/cookbook/urls.py +++ b/examples/cookbook/cookbook/urls.py @@ -1,8 +1,8 @@ -from django.urls import re_path from django.contrib import admin +from django.urls import re_path from django.views.decorators.csrf import csrf_exempt + from graphene_django.views import AsyncGraphQLView -from graphene_django.views import GraphQLView urlpatterns = [ re_path(r"^admin/", admin.site.urls), diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index 1e7bcf600..16589ebe9 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -1,15 +1,11 @@ -from django.db import connections - from asgiref.sync import sync_to_async -import inspect -from .sql.tracking import unwrap_cursor, wrap_cursor -from .exception.formating import wrap_exception -from .types import DjangoDebug +from django.db import connections from graphql.type.definition import GraphQLNonNull -from django.db.models import QuerySet - from ..utils import is_sync_function +from .exception.formating import wrap_exception +from .sql.tracking import unwrap_cursor, wrap_cursor +from .types import DjangoDebug class DjangoDebugContext: diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 50a80dcb2..d5ed46804 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,6 +1,6 @@ -import inspect from functools import partial +from asgiref.sync import sync_to_async from django.db.models.query import QuerySet from graphql_relay import ( connection_from_array_slice, @@ -9,16 +9,13 @@ offset_to_cursor, ) -from asgiref.sync import sync_to_async -from promise import Promise - from graphene import Int, NonNull from graphene.relay import ConnectionField from graphene.relay.connection import connection_adapter, page_info_adapter from graphene.types import Field, List from .settings import graphene_settings -from .utils import maybe_queryset, is_sync_function, is_running_async +from .utils import is_running_async, is_sync_function, maybe_queryset class DjangoListField(Field): diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index 4c8d82614..a7be72076 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -1,14 +1,13 @@ from collections import OrderedDict from functools import partial +from asgiref.sync import sync_to_async from django.core.exceptions import ValidationError from graphene.types.argument import to_arguments from graphene.types.enum import EnumType from graphene.utils.str_converters import to_snake_case -from asgiref.sync import sync_to_async - from ..fields import DjangoConnectionField from .utils import get_filtering_args_from_filterset, get_filterset_class diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index 105729955..f5f9b4eb9 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -1,9 +1,9 @@ from collections import OrderedDict from enum import Enum +from asgiref.sync import sync_to_async from django.shortcuts import get_object_or_404 from rest_framework import serializers -from asgiref.sync import sync_to_async import graphene from graphene.relay.mutation import ClientIDMutation @@ -12,8 +12,8 @@ from graphene.types.objecttype import yank_fields_from_attrs from ..types import ErrorType -from .serializer_converter import convert_serializer_field from ..utils import is_running_async +from .serializer_converter import convert_serializer_field class SerializerMutationOptions(MutationOptions): diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index eae5d5b9d..3f67a9f4e 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -1,21 +1,21 @@ import datetime import re -from django.db.models import Count, Prefetch -from asgiref.sync import sync_to_async, async_to_sync + import pytest +from asgiref.sync import async_to_sync from django.db.models import Count, Prefetch from graphene import List, NonNull, ObjectType, Schema, String from ..fields import DjangoListField from ..types import DjangoObjectType +from .async_test_helper import assert_async_result_equal from .models import ( Article as ArticleModel, Film as FilmModel, FilmDetails as FilmDetailsModel, Reporter as ReporterModel, ) -from .async_test_helper import assert_async_result_equal class TestDjangoListField: diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 4dd83e9fb..e8aa1f746 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -2,12 +2,12 @@ import datetime import pytest +from asgiref.sync import async_to_sync from django.db import models from django.db.models import Q from django.utils.functional import SimpleLazyObject from graphql_relay import to_global_id from pytest import raises -from asgiref.sync import sync_to_async, async_to_sync import graphene from graphene.relay import Node @@ -28,6 +28,7 @@ Reporter, ) + def test_should_query_only_fields(): with raises(Exception): @@ -1569,7 +1570,7 @@ class Query(graphene.ObjectType): expected = {"allReporters": {"edges": []}} assert not result.errors assert result.data == expected - assert_async_result_equal(schema, query, result, variable_values=dict(after=after)) + assert_async_result_equal(schema, query, result, variable_values={"after": after}) REPORTERS = [ @@ -1667,7 +1668,7 @@ class Query(graphene.ObjectType): gql_reporter["node"]["id"] for gql_reporter in gql_reporters } assert_async_result_equal( - schema, query, result2, variable_values=dict(first=4, after=last_result) + schema, query, result2, variable_values={"first": 4, "after": last_result} ) @@ -1736,7 +1737,7 @@ def test_query_first_and_last(self, graphene_settings, max_limit): def test_query_first_last_and_after(self, graphene_settings, max_limit): schema = self.setup_schema(graphene_settings, max_limit=max_limit) - query = """ + query_first_last_and_after = """ query queryAfter($after: String) { allReporters(first: 4, last: 3, after: $after) { edges { @@ -1759,12 +1760,12 @@ def test_query_first_last_and_after(self, graphene_settings, max_limit): e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 2", "First 3", "First 4"] assert_async_result_equal( - schema, query, result, variable_values=dict(after=after) + schema, query_first_last_and_after, result, variable_values={"after": after} ) def test_query_last_and_before(self, graphene_settings, max_limit): schema = self.setup_schema(graphene_settings, max_limit=max_limit) - query = """ + query_first_last_and_after = """ query queryAfter($before: String) { allReporters(last: 1, before: $before) { edges { @@ -1777,12 +1778,12 @@ def test_query_last_and_before(self, graphene_settings, max_limit): """ result = schema.execute( - query, + query_first_last_and_after, ) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 1 assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 5" - assert_async_result_equal(schema, query, result) + assert_async_result_equal(schema, query_first_last_and_after, result) before = base64.b64encode(b"arrayconnection:5").decode() result = schema.execute( @@ -1793,7 +1794,10 @@ def test_query_last_and_before(self, graphene_settings, max_limit): assert len(result.data["allReporters"]["edges"]) == 1 assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 4" assert_async_result_equal( - schema, query, result, variable_values=dict(before=before) + schema, + query_first_last_and_after, + result, + variable_values={"before": before}, ) @@ -2021,9 +2025,7 @@ class Query(graphene.ObjectType): expected_error = "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `allReporters` connection." assert len(result.errors) == 1 assert result.errors[0].message == expected_error - assert_async_result_equal( - schema, query, result, variable_values=dict(before=before) - ) + assert_async_result_equal(schema, query, result, variable_values={"before": before}) def test_connection_should_allow_offset_filtering_with_after(): @@ -2066,7 +2068,7 @@ class Query(graphene.ObjectType): } } assert result.data == expected - assert_async_result_equal(schema, query, result, variable_values=dict(after=after)) + assert_async_result_equal(schema, query, result, variable_values={"after": after}) def test_connection_should_succeed_if_last_higher_than_number_of_objects(): @@ -2097,7 +2099,7 @@ class Query(graphene.ObjectType): assert not result.errors expected = {"allReporters": {"edges": []}} assert result.data == expected - assert_async_result_equal(schema, query, result, variable_values=dict(last=2)) + assert_async_result_equal(schema, query, result, variable_values={"last": 2}) Reporter.objects.create(first_name="John", last_name="Doe") Reporter.objects.create(first_name="Some", last_name="Guy") @@ -2115,7 +2117,7 @@ class Query(graphene.ObjectType): } } assert result.data == expected - assert_async_result_equal(schema, query, result, variable_values=dict(last=2)) + assert_async_result_equal(schema, query, result, variable_values={"last": 2}) result = schema.execute(query, variable_values={"last": 4}) assert not result.errors @@ -2130,7 +2132,7 @@ class Query(graphene.ObjectType): } } assert result.data == expected - assert_async_result_equal(schema, query, result, variable_values=dict(last=4)) + assert_async_result_equal(schema, query, result, variable_values={"last": 4}) result = schema.execute(query, variable_values={"last": 20}) assert not result.errors @@ -2145,7 +2147,7 @@ class Query(graphene.ObjectType): } } assert result.data == expected - assert_async_result_equal(schema, query, result, variable_values=dict(last=20)) + assert_async_result_equal(schema, query, result, variable_values={"last": 20}) def test_should_query_nullable_foreign_key(): diff --git a/graphene_django/types.py b/graphene_django/types.py index 5dfbe5509..0f94fa271 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -16,8 +16,8 @@ DJANGO_FILTER_INSTALLED, camelize, get_model_fields, - is_valid_django_model, is_running_async, + is_valid_django_model, ) ALL_FIELDS = "__all__" diff --git a/graphene_django/utils/__init__.py b/graphene_django/utils/__init__.py index b1a11cf49..609da967c 100644 --- a/graphene_django/utils/__init__.py +++ b/graphene_django/utils/__init__.py @@ -5,10 +5,10 @@ camelize, get_model_fields, get_reverse_fields, + is_running_async, + is_sync_function, is_valid_django_model, maybe_queryset, - is_sync_function, - is_running_async, ) __all__ = [ diff --git a/graphene_django/utils/utils.py b/graphene_django/utils/utils.py index 1b9760a56..7cdaf06b7 100644 --- a/graphene_django/utils/utils.py +++ b/graphene_django/utils/utils.py @@ -153,6 +153,7 @@ def is_sync_function(func): func ) + def bypass_get_queryset(resolver): """ Adds a bypass_get_queryset attribute to the resolver, which is used to diff --git a/graphene_django/views.py b/graphene_django/views.py index 1f3a90b1d..2a63d23e1 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -2,16 +2,14 @@ import json import re import traceback - -from asyncio import gather, coroutines +from asyncio import coroutines, gather from django.db import connection, transaction from django.http import HttpResponse, HttpResponseNotAllowed from django.http.response import HttpResponseBadRequest from django.shortcuts import render -from django.utils.decorators import method_decorator +from django.utils.decorators import classonlymethod, method_decorator from django.views.decorators.csrf import ensure_csrf_cookie -from django.utils.decorators import classonlymethod from django.views.generic import View from graphql import OperationType, get_operation_ast, parse from graphql.error import GraphQLError From 45fb299398adfb68dedbecb828915efc55797a1d Mon Sep 17 00:00:00 2001 From: Josh Warwick Date: Wed, 16 Aug 2023 11:00:45 +0100 Subject: [PATCH 34/35] Fix bug when running sync view under asgi --- graphene_django/debug/middleware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index 16589ebe9..adc8d0404 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -2,7 +2,7 @@ from django.db import connections from graphql.type.definition import GraphQLNonNull -from ..utils import is_sync_function +from ..utils import is_running_async, is_sync_function from .exception.formating import wrap_exception from .sql.tracking import unwrap_cursor, wrap_cursor from .types import DjangoDebug @@ -91,7 +91,7 @@ def resolve(self, next, root, info, **args): info.parent_type.name == "Mutation", ] ): - if is_sync_function(next): + if is_sync_function(next) and is_running_async(): return sync_to_async(next)(root, info, **args) return next(root, info, **args) From c2d601c0e2ee151ae918a9c01578e5e2f17dea6f Mon Sep 17 00:00:00 2001 From: Firas Kafri <3097061+firaskafri@users.noreply.github.com> Date: Fri, 9 Feb 2024 11:58:35 +0300 Subject: [PATCH 35/35] Fix newline --- graphene_django/tests/test_fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index eac62ddaa..3b63c3aa7 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -881,4 +881,4 @@ class Query(ObjectType): TypeError, match="DjangoConnectionField only accepts DjangoObjectType types as underlying type", ): - Schema(query=Query) \ No newline at end of file + Schema(query=Query)