Repository: encode/starlette
Pull request: 🔥 Remove GraphQL support
This PR removes GraphQL entirely. I've assumed that we don't need to document the removal, just the CHANGELOG is enough (correct me otherwise).

The idea is to merge this PR before 1.0. There's no hurry on this, once we fulfill the other requirements, we can come back to this PR.

## Related issues
- Moving forward https://github.com/encode/starlette/issues/51
- Closes https://github.com/encode/starlette/issues/619

Problem statement:
GraphQL support should be a third party application.
I'd like our graphql support to move out into being a third party package.

We should then document:

* The third party package it's been moved to.
* Links to ASGI implementations with ariadne, tartiflette, strawberry.

Questions:

* Which of Ariadne, Tartiflette & Strawberry currently have provide an ASGI application, that Starlette can mount?
* Are there any other GraphQL implementations I've missed here?
* Anyone keen to take on the GraphQL implementation as a third party package?

Hints from issue discussion:
* Ariadne ASGI app: https://ariadnegraphql.org/docs/asgi
* Tatiflette: https://github.com/tartiflette/tartiflette-starlette

`strawberry` asgi app:
* https://github.com/strawberry-graphql/strawberry/blob/master/docs/05_integrations/asgi.md
> `strawberry` asgi app:
> * https://github.com/strawberry-graphql/strawberry/blob/master/docs/05_integrations/asgi.md

`​@​strawberry​.​field` forever : 💗 !
Just commenting to add some updated links to libraries that can provide GraphQL support to Starlette applications. These will be in the Starlette documentation as well:
* [Ariadne](https://ariadnegraphql.org/docs/asgi)
* [`tartiflette-asgi`](https://tartiflette.github.io/tartiflette-asgi/)
* [Strawberry](https://strawberry.rocks/docs/integrations/asgi)
* [`starlette-graphene3`](https://github.com/ciscorn/starlette-graphene3)
> > `strawberry` asgi app:
> > 
> > * https://github.com/strawberry-graphql/strawberry/blob/master/docs/05_integrations/asgi.md
> 
> `​@​strawberry​.​field` forever : 💗 !

Dead Link

> > > `strawberry` asgi app:
> > > 
> > > * https://github.com/strawberry-graphql/strawberry/blob/master/docs/05_integrations/asgi.md
> > 
> > 
> > `​@​strawberry​.​field` forever : 💗 !
> 
> Dead Link

Updated link is here: https://strawberry.rocks/docs/integrations/asgi ☺️
How easy would it be to replace the existing solution with any of the suggested alternatives? It looks like the current solution is quite strongly coupled to graphene, or am I wrong? 

The idea would be to output the schema as plain text or json to a third party library?

I would gladly see even over-simplified example to appear in the documentation because its not clear to me how everything is interacting. For example I can see in strawberry's case that:

> 
> The GraphQL app accepts two options at the moment:
> 
> * schema: mandatory, the schema created by `strawberry.Schema`.

Seems like a problem since most people using starlette probably used graphene to generate their schema. I'm personally "stuck" with graphene as I used graphene-sqlalchemy, but maybe I missed a better option to interface my SQLAlchemy models with GraphQL.

I found an example in Ariadne's documentation: [Starlette integration](https://ariadnegraphql.org/docs/0.4.0/starlette-integration) where they use the string version of the schema (aka `type_defs`). There is a discussion about this [here](https://github.com/mirumee/ariadne/issues/115). 
This issue can be closed as soon as we document our decision of deprecating our GraphQL module. 
It has been documented, we just haven't done a release in a while so the docs haven't been updated.

Commit message: Remove graphene dependency and add docs

Patch to review:
diff --git a/README.md b/README.md
index 8eedea952..a7d94904d 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,6 @@ It is production-ready, and gives you the following:
 
 * Seriously impressive performance.
 * WebSocket support.
-* GraphQL support.
 * In-process background tasks.
 * Startup and shutdown events.
 * Test client built on `requests`.
@@ -92,7 +91,6 @@ Starlette only requires `anyio`, and the following are optional:
 * [`python-multipart`][python-multipart] - Required if you want to support form parsing, with `request.form()`.
 * [`itsdangerous`][itsdangerous] - Required for `SessionMiddleware` support.
 * [`pyyaml`][pyyaml] - Required for `SchemaGenerator` support.
-* [`graphene`][graphene] - Required for `GraphQLApp` support.
 
 You can install all of these with `pip3 install starlette[full]`.
 
@@ -169,7 +167,6 @@ gunicorn -k uvicorn.workers.UvicornH11Worker ...
 [requests]: http://docs.python-requests.org/en/master/
 [jinja2]: http://jinja.pocoo.org/
 [python-multipart]: https://andrew-d.github.io/python-multipart/
-[graphene]: https://graphene-python.org/
 [itsdangerous]: https://pythonhosted.org/itsdangerous/
 [sqlalchemy]: https://www.sqlalchemy.org
 [pyyaml]: https://pyyaml.org/wiki/PyYAMLDocumentation
diff --git a/docs/graphql.md b/docs/graphql.md
index 281bdea85..466f9e30e 100644
--- a/docs/graphql.md
+++ b/docs/graphql.md
@@ -1,116 +1,10 @@
+GraphQL support in Starlette was deprecated in version 0.15.0, and removed in version 0.17.0.
 
-!!! Warning
+To not let the users lost when integrating with GraphQL, we would like to recommend the following alternatives:
 
-    GraphQL support in Starlette is **deprecated** as of version 0.15 and will
-    be removed in a future release. It is also incompatible with Python 3.10+.
-    Please consider using a third-party library to provide GraphQL support. This
-    is usually done by mounting a GraphQL ASGI application.
-    See [#619](https://github.com/encode/starlette/issues/619).
-    Some example libraries are:
+- [Ariadne](https://ariadnegraphql.org/docs/starlette-integration.html)
+- [`starlette-graphene3`](https://github.com/ciscorn/starlette-graphene3#example)
+- [Strawberry](https://strawberry.rocks/docs/integrations/starlette)
+- [`tartiflette-asgi`](https://tartiflette.github.io/tartiflette-asgi/usage/#starlette)
 
-    * [Ariadne](https://ariadnegraphql.org/docs/asgi)
-    * [`tartiflette-asgi`](https://tartiflette.github.io/tartiflette-asgi/)
-    * [Strawberry](https://strawberry.rocks/docs/integrations/asgi)
-    * [`starlette-graphene3`](https://github.com/ciscorn/starlette-graphene3)
-
-Starlette includes optional support for GraphQL, using the `graphene` library.
-
-Here's an example of integrating the support into your application.
-
-```python
-from starlette.applications import Starlette
-from starlette.routing import Route
-from starlette.graphql import GraphQLApp
-import graphene
-
-
-class Query(graphene.ObjectType):
-    hello = graphene.String(name=graphene.String(default_value="stranger"))
-
-    def resolve_hello(self, info, name):
-        return "Hello " + name
-
-routes = [
-    Route('/', GraphQLApp(schema=graphene.Schema(query=Query)))
-]
-
-app = Starlette(routes=routes)
-```
-
-If you load up the page in a browser, you'll be served the GraphiQL tool,
-which you can use to interact with your GraphQL API.
-
-
-![GraphiQL](img/graphiql.png)
-
-## Accessing request information
-
-The current request is available in the context.
-
-```python
-class Query(graphene.ObjectType):
-    user_agent = graphene.String()
-
-    def resolve_user_agent(self, info):
-        """
-        Return the User-Agent of the incoming request.
-        """
-        request = info.context["request"]
-        return request.headers.get("User-Agent", "<unknown>")
-```
-
-## Adding background tasks
-
-You can add background tasks to run once the response has been sent.
-
-```python
-class Query(graphene.ObjectType):
-    user_agent = graphene.String()
-
-    def resolve_user_agent(self, info):
-        """
-        Return the User-Agent of the incoming request.
-        """
-        user_agent = request.headers.get("User-Agent", "<unknown>")
-        background = info.context["background"]
-        background.add_task(log_user_agent, user_agent=user_agent)
-        return user_agent
-
-async def log_user_agent(user_agent):
-    ...
-```
-
-## Sync or Async executors
-
-If you're working with a standard ORM, then just use regular function calls for
-your "resolve" methods, and Starlette will manage running the GraphQL query within a
-separate thread.
-
-If you want to use an asynchronous ORM, then use "async resolve" methods, and
-make sure to setup Graphene's AsyncioExecutor using the `executor` argument.
-
-```python
-from graphql.execution.executors.asyncio import AsyncioExecutor
-from starlette.applications import Starlette
-from starlette.graphql import GraphQLApp
-from starlette.routing import Route
-import graphene
-
-
-class Query(graphene.ObjectType):
-    hello = graphene.String(name=graphene.String(default_value="stranger"))
-
-    async def resolve_hello(self, info, name):
-        # We can make asynchronous network calls here.
-        return "Hello " + name
-
-routes = [
-    # We're using `executor_class=AsyncioExecutor` here.
-    Route('/', GraphQLApp(
-        schema=graphene.Schema(query=Query),
-        executor_class=AsyncioExecutor
-    ))
-]
-
-app = Starlette(routes=routes)
-```
+Those tutorials should be enough to get you started with the GraphQL integration.
diff --git a/docs/img/graphiql.png b/docs/img/graphiql.png
deleted file mode 100644
index 7851993f7..000000000
Binary files a/docs/img/graphiql.png and /dev/null differ
diff --git a/docs/index.md b/docs/index.md
index b9692a1fb..a9ec4106f 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -162,7 +162,6 @@ gunicorn -k uvicorn.workers.UvicornH11Worker ...
 [requests]: http://docs.python-requests.org/en/master/
 [jinja2]: http://jinja.pocoo.org/
 [python-multipart]: https://andrew-d.github.io/python-multipart/
-[graphene]: https://graphene-python.org/
 [itsdangerous]: https://pythonhosted.org/itsdangerous/
 [sqlalchemy]: https://www.sqlalchemy.org
 [pyyaml]: https://pyyaml.org/wiki/PyYAMLDocumentation
diff --git a/requirements.txt b/requirements.txt
index abc7a3b0a..59cc2edf7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,4 @@
 # Optionals
-graphene; python_version<'3.10'
 itsdangerous
 jinja2
 python-multipart
diff --git a/setup.cfg b/setup.cfg
index 1266d95c8..39f67c215 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -24,8 +24,8 @@ xfail_strict=True
 filterwarnings=
     # Turn warnings that aren't filtered into exceptions
     error
-    # Deprecated GraphQL (including https://github.com/graphql-python/graphene/issues/1055)
-    ignore: GraphQLApp is deprecated and will be removed in a future release\..*:DeprecationWarning
+    # https://github.com/Tinche/aiofiles/issues/81
+    ignore: "@coroutine" decorator is deprecated.*:DeprecationWarning
     ignore: Using or importing the ABCs from 'collections' instead of from 'collections\.abc' is deprecated.*:DeprecationWarning
     ignore: The 'context' alias has been deprecated. Please use 'context_value' instead\.:DeprecationWarning
     ignore: The 'variables' alias has been deprecated. Please use 'variable_values' instead\.:DeprecationWarning
@@ -34,8 +34,3 @@ filterwarnings=
 
 [coverage:run]
 source_pkgs = starlette, tests
-# GraphQLApp incompatible with and untested on Python 3.10. It's deprecated, let's just ignore
-# coverage for it until it's gone.
-omit =
-  starlette/graphql.py
-  tests/test_graphql.py
diff --git a/setup.py b/setup.py
index 31789fe09..3b8d32e16 100644
--- a/setup.py
+++ b/setup.py
@@ -44,7 +44,6 @@ def get_long_description():
     ],
     extras_require={
         "full": [
-            "graphene; python_version<'3.10'",
             "itsdangerous",
             "jinja2",
             "python-multipart",
diff --git a/starlette/graphql.py b/starlette/graphql.py
deleted file mode 100644
index 6e5d6ec6a..000000000
--- a/starlette/graphql.py
+++ /dev/null
@@ -1,275 +0,0 @@
-import json
-import typing
-import warnings
-
-from starlette import status
-from starlette.background import BackgroundTasks
-from starlette.concurrency import run_in_threadpool
-from starlette.requests import Request
-from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse, Response
-from starlette.types import Receive, Scope, Send
-
-warnings.warn(
-    "GraphQLApp is deprecated and will be removed in a future release. "
-    "Consider using a third-party GraphQL implementation. "
-    "See https://github.com/encode/starlette/issues/619.",
-    DeprecationWarning,
-)
-
-try:
-    import graphene
-    from graphql.error import GraphQLError, format_error as format_graphql_error
-    from graphql.execution.executors.asyncio import AsyncioExecutor
-except ImportError:  # pragma: nocover
-    graphene = None
-    AsyncioExecutor = None  # type: ignore
-    format_graphql_error = None  # type: ignore
-    GraphQLError = None  # type: ignore
-
-
-class GraphQLApp:
-    def __init__(
-        self,
-        schema: "graphene.Schema",
-        executor_class: type = None,
-        graphiql: bool = True,
-    ) -> None:
-        self.schema = schema
-        self.graphiql = graphiql
-        self.executor_class = executor_class
-        self.is_async = executor_class is not None and issubclass(
-            executor_class, AsyncioExecutor
-        )
-
-    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
-        if self.executor_class is not None:
-            self.executor = self.executor_class()
-
-        request = Request(scope, receive=receive)
-        response = await self.handle_graphql(request)
-        await response(scope, receive, send)
-
-    async def handle_graphql(self, request: Request) -> Response:
-        if request.method in ("GET", "HEAD"):
-            if "text/html" in request.headers.get("Accept", ""):
-                if not self.graphiql:
-                    return PlainTextResponse(
-                        "Not Found", status_code=status.HTTP_404_NOT_FOUND
-                    )
-                return await self.handle_graphiql(request)
-
-            data: typing.Mapping[str, typing.Any] = request.query_params
-
-        elif request.method == "POST":
-            content_type = request.headers.get("Content-Type", "")
-
-            if "application/json" in content_type:
-                data = await request.json()
-            elif "application/graphql" in content_type:
-                body = await request.body()
-                text = body.decode()
-                data = {"query": text}
-            elif "query" in request.query_params:
-                data = request.query_params
-            else:
-                return PlainTextResponse(
-                    "Unsupported Media Type",
-                    status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
-                )
-
-        else:
-            return PlainTextResponse(
-                "Method Not Allowed", status_code=status.HTTP_405_METHOD_NOT_ALLOWED
-            )
-
-        try:
-            query = data["query"]
-            variables = data.get("variables")
-            operation_name = data.get("operationName")
-        except KeyError:
-            return PlainTextResponse(
-                "No GraphQL query found in the request",
-                status_code=status.HTTP_400_BAD_REQUEST,
-            )
-
-        background = BackgroundTasks()
-        context = {"request": request, "background": background}
-
-        result = await self.execute(
-            query, variables=variables, context=context, operation_name=operation_name
-        )
-        error_data = (
-            [format_graphql_error(err) for err in result.errors]
-            if result.errors
-            else None
-        )
-        response_data = {"data": result.data}
-        if error_data:
-            response_data["errors"] = error_data
-        status_code = (
-            status.HTTP_400_BAD_REQUEST if result.errors else status.HTTP_200_OK
-        )
-
-        return JSONResponse(
-            response_data, status_code=status_code, background=background
-        )
-
-    async def execute(  # type: ignore
-        self, query, variables=None, context=None, operation_name=None
-    ):
-        if self.is_async:
-            return await self.schema.execute(
-                query,
-                variables=variables,
-                operation_name=operation_name,
-                executor=self.executor,
-                return_promise=True,
-                context=context,
-            )
-        else:
-            return await run_in_threadpool(
-                self.schema.execute,
-                query,
-                variables=variables,
-                operation_name=operation_name,
-                context=context,
-            )
-
-    async def handle_graphiql(self, request: Request) -> Response:
-        text = GRAPHIQL.replace("{{REQUEST_PATH}}", json.dumps(request.url.path))
-        return HTMLResponse(text)
-
-
-GRAPHIQL = """
-<!--
- *  Copyright (c) Facebook, Inc.
- *  All rights reserved.
- *
- *  This source code is licensed under the license found in the
- *  LICENSE file in the root directory of this source tree.
--->
-<!DOCTYPE html>
-<html>
-  <head>
-    <style>
-      body {
-        height: 100%;
-        margin: 0;
-        width: 100%;
-        overflow: hidden;
-      }
-      #graphiql {
-        height: 100vh;
-      }
-    </style>
-    <!--
-      This GraphiQL example depends on Promise and fetch, which are available in
-      modern browsers, but can be "polyfilled" for older browsers.
-      GraphiQL itself depends on React DOM.
-      If you do not want to rely on a CDN, you can host these files locally or
-      include them directly in your favored resource bunder.
-    -->
-    <link href="//cdn.jsdelivr.net/npm/graphiql@0.12.0/graphiql.css" rel="stylesheet"/>
-    <script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script>
-    <script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script>
-    <script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script>
-    <script src="//cdn.jsdelivr.net/npm/graphiql@0.12.0/graphiql.min.js"></script>
-  </head>
-  <body>
-    <div id="graphiql">Loading...</div>
-    <script>
-      /**
-       * This GraphiQL example illustrates how to use some of GraphiQL's props
-       * in order to enable reading and updating the URL parameters, making
-       * link sharing of queries a little bit easier.
-       *
-       * This is only one example of this kind of feature, GraphiQL exposes
-       * various React params to enable interesting integrations.
-       */
-      // Parse the search string to get url parameters.
-      var search = window.location.search;
-      var parameters = {};
-      search.substr(1).split('&').forEach(function (entry) {
-        var eq = entry.indexOf('=');
-        if (eq >= 0) {
-          parameters[decodeURIComponent(entry.slice(0, eq))] =
-            decodeURIComponent(entry.slice(eq + 1));
-        }
-      });
-      // if variables was provided, try to format it.
-      if (parameters.variables) {
-        try {
-          parameters.variables =
-            JSON.stringify(JSON.parse(parameters.variables), null, 2);
-        } catch (e) {
-          // Do nothing, we want to display the invalid JSON as a string, rather
-          // than present an error.
-        }
-      }
-      // When the query and variables string is edited, update the URL bar so
-      // that it can be easily shared
-      function onEditQuery(newQuery) {
-        parameters.query = newQuery;
-        updateURL();
-      }
-      function onEditVariables(newVariables) {
-        parameters.variables = newVariables;
-        updateURL();
-      }
-      function onEditOperationName(newOperationName) {
-        parameters.operationName = newOperationName;
-        updateURL();
-      }
-      function updateURL() {
-        var newSearch = '?' + Object.keys(parameters).filter(function (key) {
-          return Boolean(parameters[key]);
-        }).map(function (key) {
-          return encodeURIComponent(key) + '=' +
-            encodeURIComponent(parameters[key]);
-        }).join('&');
-        history.replaceState(null, null, newSearch);
-      }
-      // Defines a GraphQL fetcher using the fetch API. You're not required to
-      // use fetch, and could instead implement graphQLFetcher however you like,
-      // as long as it returns a Promise or Observable.
-      function graphQLFetcher(graphQLParams) {
-        // This example expects a GraphQL server at the path /graphql.
-        // Change this to point wherever you host your GraphQL server.
-        return fetch({{REQUEST_PATH}}, {
-          method: 'post',
-          headers: {
-            'Accept': 'application/json',
-            'Content-Type': 'application/json',
-          },
-          body: JSON.stringify(graphQLParams),
-          credentials: 'include',
-        }).then(function (response) {
-          return response.text();
-        }).then(function (responseBody) {
-          try {
-            return JSON.parse(responseBody);
-          } catch (error) {
-            return responseBody;
-          }
-        });
-      }
-      // Render <GraphiQL /> into the body.
-      // See the README in the top level of this module to learn more about
-      // how you can customize GraphiQL by providing different values or
-      // additional child elements.
-      ReactDOM.render(
-        React.createElement(GraphiQL, {
-          fetcher: graphQLFetcher,
-          query: parameters.query,
-          variables: parameters.variables,
-          operationName: parameters.operationName,
-          onEditQuery: onEditQuery,
-          onEditVariables: onEditVariables,
-          onEditOperationName: onEditOperationName
-        }),
-        document.getElementById('graphiql')
-      );
-    </script>
-  </body>
-</html>
-"""  # noqa: E501
diff --git a/tests/conftest.py b/tests/conftest.py
index bb68aa5e2..4f210b3bd 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,12 +1,9 @@
 import functools
-import sys
 
 import pytest
 
 from starlette.testclient import TestClient
 
-collect_ignore = ["test_graphql.py"] if sys.version_info >= (3, 10) else []
-
 
 @pytest.fixture
 def no_trio_support(anyio_backend_name):
diff --git a/tests/test_graphql.py b/tests/test_graphql.py
deleted file mode 100644
index 8492439f8..000000000
--- a/tests/test_graphql.py
+++ /dev/null
@@ -1,152 +0,0 @@
-import graphene
-import pytest
-from graphql.execution.executors.asyncio import AsyncioExecutor
-
-from starlette.applications import Starlette
-from starlette.datastructures import Headers
-from starlette.graphql import GraphQLApp
-
-
-class FakeAuthMiddleware:
-    def __init__(self, app) -> None:
-        self.app = app
-
-    async def __call__(self, scope, receive, send):
-        headers = Headers(scope=scope)
-        scope["user"] = "Jane" if headers.get("Authorization") == "Bearer 123" else None
-        await self.app(scope, receive, send)
-
-
-class Query(graphene.ObjectType):
-    hello = graphene.String(name=graphene.String(default_value="stranger"))
-    whoami = graphene.String()
-
-    def resolve_hello(self, info, name):
-        return "Hello " + name
-
-    def resolve_whoami(self, info):
-        return (
-            "a mystery"
-            if info.context["request"]["user"] is None
-            else info.context["request"]["user"]
-        )
-
-
-schema = graphene.Schema(query=Query)
-
-
-@pytest.fixture
-def client(test_client_factory):
-    app = GraphQLApp(schema=schema, graphiql=True)
-    return test_client_factory(app)
-
-
-def test_graphql_get(client):
-    response = client.get("/?query={ hello }")
-    assert response.status_code == 200
-    assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_post(client):
-    response = client.post("/?query={ hello }")
-    assert response.status_code == 200
-    assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_post_json(client):
-    response = client.post("/", json={"query": "{ hello }"})
-    assert response.status_code == 200
-    assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_post_graphql(client):
-    response = client.post(
-        "/", data="{ hello }", headers={"content-type": "application/graphql"}
-    )
-    assert response.status_code == 200
-    assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_post_invalid_media_type(client):
-    response = client.post("/", data="{ hello }", headers={"content-type": "dummy"})
-    assert response.status_code == 415
-    assert response.text == "Unsupported Media Type"
-
-
-def test_graphql_put(client):
-    response = client.put("/", json={"query": "{ hello }"})
-    assert response.status_code == 405
-    assert response.text == "Method Not Allowed"
-
-
-def test_graphql_no_query(client):
-    response = client.get("/")
-    assert response.status_code == 400
-    assert response.text == "No GraphQL query found in the request"
-
-
-def test_graphql_invalid_field(client):
-    response = client.post("/", json={"query": "{ dummy }"})
-    assert response.status_code == 400
-    assert response.json() == {
-        "data": None,
-        "errors": [
-            {
-                "locations": [{"column": 3, "line": 1}],
-                "message": 'Cannot query field "dummy" on type "Query".',
-            }
-        ],
-    }
-
-
-def test_graphiql_get(client):
-    response = client.get("/", headers={"accept": "text/html"})
-    assert response.status_code == 200
-    assert "<!DOCTYPE html>" in response.text
-
-
-def test_graphiql_not_found(test_client_factory):
-    app = GraphQLApp(schema=schema, graphiql=False)
-    client = test_client_factory(app)
-    response = client.get("/", headers={"accept": "text/html"})
-    assert response.status_code == 404
-    assert response.text == "Not Found"
-
-
-def test_add_graphql_route(test_client_factory):
-    app = Starlette()
-    app.add_route("/", GraphQLApp(schema=schema))
-    client = test_client_factory(app)
-    response = client.get("/?query={ hello }")
-    assert response.status_code == 200
-    assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_context(test_client_factory):
-    app = Starlette()
-    app.add_middleware(FakeAuthMiddleware)
-    app.add_route("/", GraphQLApp(schema=schema))
-    client = test_client_factory(app)
-    response = client.post(
-        "/", json={"query": "{ whoami }"}, headers={"Authorization": "Bearer 123"}
-    )
-    assert response.status_code == 200
-    assert response.json() == {"data": {"whoami": "Jane"}}
-
-
-class ASyncQuery(graphene.ObjectType):
-    hello = graphene.String(name=graphene.String(default_value="stranger"))
-
-    async def resolve_hello(self, info, name):
-        return "Hello " + name
-
-
-async_schema = graphene.Schema(query=ASyncQuery)
-async_app = GraphQLApp(schema=async_schema, executor_class=AsyncioExecutor)
-
-
-def test_graphql_async(no_trio_support, test_client_factory):
-    client = test_client_factory(async_app)
-    response = client.get("/?query={ hello }")
-    assert response.status_code == 200
-    assert response.json() == {"data": {"hello": "Hello stranger"}}


Code review comments:
diff_hunk: @@ -24,8 +24,8 @@ xfail_strict=True
 filterwarnings=
     # Turn warnings that aren't filtered into exceptions
     error
-    # Deprecated GraphQL (including https://github.com/graphql-python/graphene/issues/1055)
-    ignore: GraphQLApp is deprecated and will be removed in a future release\..*:DeprecationWarning
+    # https://github.com/Tinche/aiofiles/issues/81
+    ignore: "@coroutine" decorator is deprecated.*:DeprecationWarning
path: setup.cfg
text: @user1:
Is this still a thing? I thought we got rid of our dependency on aiofiles when we moved to AnyIO.

@author:
This is probably the result of a bad rebase strategy. :sweat_smile: 

I've removed and double-checked everything else :+1: 

Thanks!

diff_hunk: @@ -1,116 +1,10 @@
+GraphQL support in Starlette was deprecated in version 0.15.0, and removed in version 0.17.0.
 
-!!! Warning
+To not let the users lost when integrating with GraphQL, we would like to recommend the following alternatives:
 
-    GraphQL support in Starlette is **deprecated** as of version 0.15 and will
-    be removed in a future release. It is also incompatible with Python 3.10+.
-    Please consider using a third-party library to provide GraphQL support. This
-    is usually done by mounting a GraphQL ASGI application.
-    See [#619](https://github.com/encode/starlette/issues/619).
-    Some example libraries are:
+- [Ariadne](https://ariadnegraphql.org/docs/starlette-integration.html)
+- [`starlette-graphene3`](https://github.com/ciscorn/starlette-graphene3#example)
+- [Strawberry](https://strawberry.rocks/docs/integrations/starlette)
+- [`tartiflette-asgi`](https://tartiflette.github.io/tartiflette-asgi/usage/#starlette)
 
-    * [Ariadne](https://ariadnegraphql.org/docs/asgi)
-    * [`tartiflette-asgi`](https://tartiflette.github.io/tartiflette-asgi/)
-    * [Strawberry](https://strawberry.rocks/docs/integrations/asgi)
-    * [`starlette-graphene3`](https://github.com/ciscorn/starlette-graphene3)
-
-Starlette includes optional support for GraphQL, using the `graphene` library.
-
-Here's an example of integrating the support into your application.
-
-```python
-from starlette.applications import Starlette
-from starlette.routing import Route
-from starlette.graphql import GraphQLApp
-import graphene
-
-
-class Query(graphene.ObjectType):
-    hello = graphene.String(name=graphene.String(default_value="stranger"))
-
-    def resolve_hello(self, info, name):
-        return "Hello " + name
-
-routes = [
-    Route('/', GraphQLApp(schema=graphene.Schema(query=Query)))
-]
-
-app = Starlette(routes=routes)
-```
-
-If you load up the page in a browser, you'll be served the GraphiQL tool,
-which you can use to interact with your GraphQL API.
-
-
-![GraphiQL](img/graphiql.png)
-
-## Accessing request information
-
-The current request is available in the context.
-
-```python
-class Query(graphene.ObjectType):
-    user_agent = graphene.String()
-
-    def resolve_user_agent(self, info):
-        """
-        Return the User-Agent of the incoming request.
-        """
-        request = info.context["request"]
-        return request.headers.get("User-Agent", "<unknown>")
-```
-
-## Adding background tasks
-
-You can add background tasks to run once the response has been sent.
-
-```python
-class Query(graphene.ObjectType):
-    user_agent = graphene.String()
-
-    def resolve_user_agent(self, info):
-        """
-        Return the User-Agent of the incoming request.
-        """
-        user_agent = request.headers.get("User-Agent", "<unknown>")
-        background = info.context["background"]
-        background.add_task(log_user_agent, user_agent=user_agent)
-        return user_agent
-
-async def log_user_agent(user_agent):
-    ...
-```
-
-## Sync or Async executors
-
-If you're working with a standard ORM, then just use regular function calls for
-your "resolve" methods, and Starlette will manage running the GraphQL query within a
-separate thread.
-
-If you want to use an asynchronous ORM, then use "async resolve" methods, and
-make sure to setup Graphene's AsyncioExecutor using the `executor` argument.
-
-```python
-from graphql.execution.executors.asyncio import AsyncioExecutor
-from starlette.applications import Starlette
-from starlette.graphql import GraphQLApp
-from starlette.routing import Route
-import graphene
-
-
-class Query(graphene.ObjectType):
-    hello = graphene.String(name=graphene.String(default_value="stranger"))
-
-    async def resolve_hello(self, info, name):
-        # We can make asynchronous network calls here.
-        return "Hello " + name
-
-routes = [
-    # We're using `executor_class=AsyncioExecutor` here.
-    Route('/', GraphQLApp(
-        schema=graphene.Schema(query=Query),
-        executor_class=AsyncioExecutor
-    ))
-]
-
-app = Starlette(routes=routes)
-```
+Those tutorials should be enough to get you started with the GraphQL integration.
path: docs/graphql.md
text: @user1:
```suggestion
```