Repository: mirumee/ariadne

Issue:
Add `execution_context_class` to ASGI and WSGI GraphQL apps
#968 added support for `execution_context_class` customization in our `graphql` wrappers around `execute`, but there's no way currently to set those on GraphQL apps.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mirumee/ariadne/pull/980?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee) Report
> Merging [#980](https://codecov.io/gh/mirumee/ariadne/pull/980?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee) (eef6107) into [master](https://codecov.io/gh/mirumee/ariadne/commit/c6a37c90a71c6f7260b7980f89e4f065573f8266?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee) (c6a37c9) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master     #980   +/-   ##
=======================================
  Coverage   98.40%   98.40%           
=======================================
  Files         103      103           
  Lines        6393     6415   +22     
=======================================
+ Hits         6291     6313   +22     
  Misses        102      102           
```


| [Impacted Files](https://codecov.io/gh/mirumee/ariadne/pull/980?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee) | Coverage Δ | |
|---|---|---|
| [ariadne/asgi/handlers/http.py](https://codecov.io/gh/mirumee/ariadne/pull/980/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee#diff-YXJpYWRuZS9hc2dpL2hhbmRsZXJzL2h0dHAucHk=) | `97.19% <ø> (ø)` | |
| [ariadne/asgi/graphql.py](https://codecov.io/gh/mirumee/ariadne/pull/980/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee#diff-YXJpYWRuZS9hc2dpL2dyYXBocWwucHk=) | `96.15% <100.00%> (ø)` | |
| [ariadne/asgi/handlers/base.py](https://codecov.io/gh/mirumee/ariadne/pull/980/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee#diff-YXJpYWRuZS9hc2dpL2hhbmRsZXJzL2Jhc2UucHk=) | `100.00% <100.00%> (ø)` | |
| [ariadne/wsgi.py](https://codecov.io/gh/mirumee/ariadne/pull/980/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee#diff-YXJpYWRuZS93c2dpLnB5) | `95.00% <100.00%> (+0.03%)` | :arrow_up: |
| [tests/wsgi/test\_configuration.py](https://codecov.io/gh/mirumee/ariadne/pull/980/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee#diff-dGVzdHMvd3NnaS90ZXN0X2NvbmZpZ3VyYXRpb24ucHk=) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cae3086..027bb41 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@
 - Added support for `@tag` directive used by Apollo Federation.
 - Updated `starlette` dependency in setup.py to `<1.0`.
 - Moved project configuration from `setup.py` to `pyproject.toml`.
-- Added `execution_context_class` option.
+- Added `execution_context_class` option to ASGI and WSGI `applications`.
 - Added `query_parser` option to ASGI and WSGI `GraphQL` applications that enables query parsing customization.
 - Added Python 3.11 support.
 
diff --git a/ariadne/asgi/graphql.py b/ariadne/asgi/graphql.py
index 272fd6d..3ad62ce 100644
--- a/ariadne/asgi/graphql.py
+++ b/ariadne/asgi/graphql.py
@@ -1,7 +1,7 @@
 from logging import Logger, LoggerAdapter
-from typing import Optional, Union
+from typing import Optional, Type, Union
 
-from graphql import GraphQLSchema
+from graphql import ExecutionContext, GraphQLSchema
 from starlette.types import Receive, Scope, Send
 
 from ..explorer import Explorer, ExplorerGraphiQL
@@ -34,6 +34,7 @@ class GraphQL:
         explorer: Optional[Explorer] = None,
         logger: Union[None, str, Logger, LoggerAdapter] = None,
         error_formatter: ErrorFormatter = format_error,
+        execution_context_class: Optional[Type[ExecutionContext]] = None,
         http_handler: Optional[GraphQLHTTPHandler] = None,
         websocket_handler: Optional[GraphQLWebsocketHandler] = None,
     ) -> None:
@@ -61,6 +62,7 @@ class GraphQL:
             explorer,
             logger,
             error_formatter,
+            execution_context_class,
         )
         self.websocket_handler.configure(
             schema,
@@ -73,6 +75,7 @@ class GraphQL:
             explorer,
             logger,
             error_formatter,
+            execution_context_class,
             http_handler=self.http_handler,
         )
 
diff --git a/ariadne/asgi/handlers/base.py b/ariadne/asgi/handlers/base.py
index 2ec6df3..48fa219 100644
--- a/ariadne/asgi/handlers/base.py
+++ b/ariadne/asgi/handlers/base.py
@@ -1,9 +1,9 @@
 from abc import ABC, abstractmethod
 from inspect import isawaitable
 from logging import Logger, LoggerAdapter
-from typing import Any, Optional, Union
+from typing import Any, Optional, Type, Union
 
-from graphql import DocumentNode, GraphQLSchema
+from graphql import DocumentNode, ExecutionContext, GraphQLSchema
 from starlette.types import Receive, Scope, Send
 
 from ...explorer import Explorer
@@ -34,6 +34,7 @@ class GraphQLHandler(ABC):
         self.root_value: Optional[RootValue] = None
         self.query_parser: Optional[QueryParser] = None
         self.validation_rules: Optional[ValidationRules] = None
+        self.execution_context_class: Optional[Type[ExecutionContext]] = None
 
     @abstractmethod
     async def handle(self, scope: Scope, receive: Receive, send: Send):
@@ -51,10 +52,12 @@ class GraphQLHandler(ABC):
         explorer: Optional[Explorer] = None,
         logger: Union[None, str, Logger, LoggerAdapter] = None,
         error_formatter: ErrorFormatter = format_error,
+        execution_context_class: Optional[Type[ExecutionContext]] = None,
     ):
         self.context_value = context_value
         self.debug = debug
         self.error_formatter = error_formatter
+        self.execution_context_class = execution_context_class
         self.introspection = introspection
         self.explorer = explorer
         self.logger = logger
diff --git a/ariadne/asgi/handlers/http.py b/ariadne/asgi/handlers/http.py
index 3f80118..91e1ad6 100644
--- a/ariadne/asgi/handlers/http.py
+++ b/ariadne/asgi/handlers/http.py
@@ -112,6 +112,7 @@ class GraphQLHTTPHandler(GraphQLHttpHandlerBase):
             error_formatter=self.error_formatter,
             extensions=extensions,
             middleware=middleware,
+            execution_context_class=self.execution_context_class,
         )
 
     async def graphql_http_server(self, request: Request) -> Response:
diff --git a/ariadne/wsgi.py b/ariadne/wsgi.py
index 79fd84e..54ebbb5 100644
--- a/ariadne/wsgi.py
+++ b/ariadne/wsgi.py
@@ -1,8 +1,8 @@
 import json
 from inspect import isawaitable
-from typing import Any, Callable, Dict, List, Optional, Union, cast
+from typing import Any, Callable, Dict, List, Optional, Type, Union, cast
 
-from graphql import GraphQLError, GraphQLSchema
+from graphql import ExecutionContext, GraphQLError, GraphQLSchema
 from graphql.execution import Middleware, MiddlewareManager
 
 from .constants import (
@@ -65,6 +65,7 @@ class GraphQL:
         error_formatter: ErrorFormatter = format_error,
         extensions: Optional[Extensions] = None,
         middleware: Optional[Middlewares] = None,
+        execution_context_class: Optional[Type[ExecutionContext]] = None,
     ) -> None:
         self.context_value = context_value
         self.root_value = root_value
@@ -76,6 +77,7 @@ class GraphQL:
         self.error_formatter = error_formatter
         self.extensions = extensions
         self.middleware = middleware
+        self.execution_context_class = execution_context_class
         self.schema = schema
 
         if explorer:
@@ -214,6 +216,7 @@ class GraphQL:
             error_formatter=self.error_formatter,
             extensions=extensions,
             middleware=middleware,
+            execution_context_class=self.execution_context_class,
         )
 
     def get_context_for_request(self, environ: dict) -> Optional[ContextValue]:


Test patch:
diff --git a/tests/wsgi/test_configuration.py b/tests/wsgi/test_configuration.py
index 95f2d89..47f8608 100644
--- a/tests/wsgi/test_configuration.py
+++ b/tests/wsgi/test_configuration.py
@@ -1,15 +1,25 @@
 import json
+import sys
 from io import BytesIO
 from unittest.mock import ANY, Mock
 
+import pytest
 from graphql import parse
 from werkzeug.test import Client
 from werkzeug.wrappers import Response
 
+from ariadne import QueryType, make_executable_schema
 from ariadne.constants import DATA_TYPE_JSON
 from ariadne.types import ExtensionSync
 from ariadne.wsgi import GraphQL
 
+PY_37 = sys.version_info < (3, 8)
+
+if not PY_37:
+    # Sync dataloader is python 3.8 and later only
+    # pylint: disable=import-error
+    from graphql_sync_dataloaders import DeferredExecutionContext, SyncDataLoader
+
 
 # Add TestClient to keep test similar to ASGI
 class TestClient(Client):
@@ -205,3 +215,33 @@ def test_middleware_function_result_is_passed_to_query_executor(schema):
     app = GraphQL(schema, middleware=get_middleware)
     _, result = app.execute_query({}, {"query": '{ hello(name: "BOB") }'})
     assert result == {"data": {"hello": "**Hello, BOB!**"}}
+
+
+@pytest.mark.skipif(PY_37, reason="requires python 3.8")
+def test_wsgi_app_supports_sync_dataloader_with_custom_execution_context():
+    type_defs = """
+        type Query {
+            test(arg: ID!): String!
+        }
+    """
+
+    def dataloader_fn(keys):
+        return keys
+
+    dataloader = SyncDataLoader(dataloader_fn)
+
+    query = QueryType()
+    query.set_field("test", lambda *_, arg: dataloader.load(arg))
+
+    schema = make_executable_schema(
+        type_defs,
+        [query],
+    )
+
+    app = GraphQL(schema, execution_context_class=DeferredExecutionContext)
+    client = TestClient(app)
+
+    response = client.post(
+        "/", json={"query": "{ test1: test(arg: 1), test2: test(arg: 2) }"}
+    )
+    assert response.json == {"data": {"test1": "1", "test2": "2"}}