Repository: mirumee/ariadne

Issue:
Pass operation name and variables to root value resolver
Split form #843

Callable root value resolver becomes really useful in scenarios like GraphQL proxies, but it should have easy access to operation name and variables dict for that.

Plan is to change root value callable to support those as extra arguments, effectively changing type to that:

```python
RootValue = Union[
    Any,
    Callable[[Optional[Any], DocumentNode], Any],
    Callable[[Optional[Any], Optional[str], Optional[dict], DocumentNode], Any],
]
```

Comments:

Patch:
diff --git a/.pylintrc b/.pylintrc
index 02b186f..46abd67 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -3,7 +3,7 @@ ignore=snapshots
 load-plugins=pylint.extensions.bad_builtin, pylint.extensions.mccabe
 
 [MESSAGES CONTROL]
-disable=C0103, C0111, C0209, C0412, I0011, R0101, R0801, R0901, R0902, R0903, R0912, R0913, R0914, R0915, R1260, W0231, W0621, W0703
+disable=C0103, C0111, C0209, C0412, I0011, R0101, R0801, R0901, R0902, R0903, R0912, R0913, R0914, R0915, R1260, W0231, W0511, W0621, W0703
 
 [SIMILARITIES]
 ignore-imports=yes
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7bf3892..6dcf457 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,10 +10,11 @@
 - 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 to ASGI and WSGI `applications`.
+- Changed `root_value` option in ASGI and WSGI applications for callables to take operation and and variables in addition to context and parsed query.
+- 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.
 - Changed `middleware` option to work with callable or list of middlewares instead of `MiddlewareManager` instance.
-- Added `middleware_manager_class` option to ASGI and WSGI `applications`.
+- Added `middleware_manager_class` option to ASGI and WSGI applications.
 - Added Python 3.11 support.
 
 
diff --git a/ariadne/graphql.py b/ariadne/graphql.py
index 834d2f0..fc1d493 100644
--- a/ariadne/graphql.py
+++ b/ariadne/graphql.py
@@ -13,6 +13,7 @@ from typing import (
     cast,
     Union,
 )
+from warnings import warn
 
 from graphql import (
     DocumentNode,
@@ -46,6 +47,16 @@ from .types import (
 from .validation.introspection_disabled import IntrospectionDisabledRule
 
 
+def root_value_two_args_deprecated():  # TODO: remove in 0.19
+    warn(
+        "'root_value(context, document)' has been deprecated and will raise a type "
+        "error in Ariadne 0.18, update definition to "
+        "'root_value(context, operation_name, variables, document)'.",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+
+
 async def graphql(
     schema: GraphQLSchema,
     data: Any,
@@ -99,7 +110,14 @@ async def graphql(
                 )
 
             if callable(root_value):
-                root_value = root_value(context_value, document)
+                try:
+                    root_value = root_value(  # type: ignore
+                        context_value, operation_name, variables, document
+                    )
+                except TypeError:  # TODO: remove in 0.19
+                    root_value_two_args_deprecated()
+                    root_value = root_value(context_value, document)  # type: ignore
+
                 if isawaitable(root_value):
                     root_value = await root_value
 
@@ -190,7 +208,14 @@ def graphql_sync(
                 )
 
             if callable(root_value):
-                root_value = root_value(context_value, document)
+                try:
+                    root_value = root_value(  # type: ignore
+                        context_value, operation_name, variables, document
+                    )
+                except TypeError:  # TODO: remove in 0.19
+                    root_value_two_args_deprecated()
+                    root_value = root_value(context_value, document)  # type: ignore
+
                 if isawaitable(root_value):
                     ensure_future(root_value).cancel()
                     raise RuntimeError(
@@ -280,7 +305,14 @@ async def subscribe(
             )
 
         if callable(root_value):
-            root_value = root_value(context_value, document)
+            try:
+                root_value = root_value(  # type: ignore
+                    context_value, operation_name, variables, document
+                )
+            except TypeError:  # TODO: remove in 0.19
+                root_value_two_args_deprecated()
+                root_value = root_value(context_value, document)  # type: ignore
+
             if isawaitable(root_value):
                 root_value = await root_value
 
diff --git a/ariadne/types.py b/ariadne/types.py
index 9368464..74046b9 100644
--- a/ariadne/types.py
+++ b/ariadne/types.py
@@ -40,7 +40,11 @@ Subscriber = Callable[..., AsyncGenerator]
 ErrorFormatter = Callable[[GraphQLError, bool], dict]
 
 ContextValue = Union[Any, Callable[[Any], Any]]
-RootValue = Union[Any, Callable[[Optional[Any], DocumentNode], Any]]
+RootValue = Union[
+    Any,
+    Callable[[Optional[Any], DocumentNode], Any],  # TODO: remove in 0.19
+    Callable[[Optional[Any], Optional[str], Optional[dict], DocumentNode], Any],
+]
 
 QueryParser = Callable[[ContextValue, Dict[str, Any]], DocumentNode]
 


Test patch:
diff --git a/tests/asgi/test_configuration.py b/tests/asgi/test_configuration.py
index 9f1ceba..f7c007d 100644
--- a/tests/asgi/test_configuration.py
+++ b/tests/asgi/test_configuration.py
@@ -111,6 +111,21 @@ def test_custom_root_value_function_is_called_by_query(schema):
     get_root_value.assert_called_once()
 
 
+def test_custom_deprecated_root_value_function_raises_warning_by_query(
+    schema,
+):
+    def get_root_value(_context, _document):
+        return True
+
+    app = GraphQL(
+        schema, context_value={"test": "TEST-CONTEXT"}, root_value=get_root_value
+    )
+    client = TestClient(app)
+
+    with pytest.deprecated_call():
+        client.post("/", json={"query": "{ status }"})
+
+
 def test_custom_root_value_function_is_called_by_subscription(schema):
     get_root_value = Mock(return_value=True)
     app = GraphQL(schema, root_value=get_root_value)
@@ -131,6 +146,29 @@ def test_custom_root_value_function_is_called_by_subscription(schema):
         get_root_value.assert_called_once()
 
 
+def test_custom_deprecated_root_value_function_raises_warning_by_subscription(schema):
+    def get_root_value(_context, _document):
+        return True
+
+    app = GraphQL(schema, root_value=get_root_value)
+    client = TestClient(app)
+
+    with pytest.deprecated_call():
+        with client.websocket_connect("/", ["graphql-ws"]) as ws:
+            ws.send_json({"type": GraphQLWSHandler.GQL_CONNECTION_INIT})
+            ws.send_json(
+                {
+                    "type": GraphQLWSHandler.GQL_START,
+                    "id": "test1",
+                    "payload": {"query": "subscription { ping }"},
+                }
+            )
+            response = ws.receive_json()
+            assert response["type"] == GraphQLWSHandler.GQL_CONNECTION_ACK
+            response = ws.receive_json()
+            assert response["type"] == GraphQLWSHandler.GQL_DATA
+
+
 def test_custom_root_value_function_is_called_by_subscription_graphql_transport_ws(
     schema,
 ):
@@ -156,6 +194,34 @@ def test_custom_root_value_function_is_called_by_subscription_graphql_transport_
         get_root_value.assert_called_once()
 
 
+def test_custom_deprecated_root_value_function_raises_warning_by_subscription_graphql_transport_ws(
+    schema,
+):
+    def get_root_value(_context, _document):
+        return True
+
+    websocket_handler = GraphQLTransportWSHandler()
+    app = GraphQL(
+        schema, root_value=get_root_value, websocket_handler=websocket_handler
+    )
+    client = TestClient(app)
+
+    with pytest.deprecated_call():
+        with client.websocket_connect("/", ["graphql-transport-ws"]) as ws:
+            ws.send_json({"type": GraphQLTransportWSHandler.GQL_CONNECTION_INIT})
+            ws.send_json(
+                {
+                    "type": GraphQLTransportWSHandler.GQL_SUBSCRIBE,
+                    "id": "test1",
+                    "payload": {"query": "subscription { ping }"},
+                }
+            )
+            response = ws.receive_json()
+            assert response["type"] == GraphQLTransportWSHandler.GQL_CONNECTION_ACK
+            response = ws.receive_json()
+            assert response["type"] == GraphQLTransportWSHandler.GQL_NEXT
+
+
 def test_custom_root_value_function_is_called_with_context_value(schema):
     get_root_value = Mock(return_value=True)
     app = GraphQL(
@@ -163,7 +229,7 @@ def test_custom_root_value_function_is_called_with_context_value(schema):
     )
     client = TestClient(app)
     client.post("/", json={"query": "{ status }"})
-    get_root_value.assert_called_once_with({"test": "TEST-CONTEXT"}, ANY)
+    get_root_value.assert_called_once_with({"test": "TEST-CONTEXT"}, None, None, ANY)
 
 
 def test_custom_query_parser_is_used_for_http_query(schema):
diff --git a/tests/wsgi/test_configuration.py b/tests/wsgi/test_configuration.py
index 47f8608..ed0a802 100644
--- a/tests/wsgi/test_configuration.py
+++ b/tests/wsgi/test_configuration.py
@@ -76,7 +76,21 @@ def test_custom_root_value_function_is_called_with_context_value(schema):
         schema, context_value={"test": "TEST-CONTEXT"}, root_value=get_root_value
     )
     app.execute_query({}, {"query": "{ status }"})
-    get_root_value.assert_called_once_with({"test": "TEST-CONTEXT"}, ANY)
+    get_root_value.assert_called_once_with({"test": "TEST-CONTEXT"}, None, None, ANY)
+
+
+def test_warning_is_raised_if_custom_root_value_function_has_deprecated_signature(
+    schema,
+):
+    def get_root_value(_context, _document):
+        return True
+
+    app = GraphQL(
+        schema, context_value={"test": "TEST-CONTEXT"}, root_value=get_root_value
+    )
+
+    with pytest.deprecated_call():
+        app.execute_query({}, {"query": "{ status }"})
 
 
 def test_custom_query_parser_is_used(schema):