]> git.ipfire.org Git - thirdparty/starlette.git/commitdiff
🔥 Remove GraphQL support (#1198)
authorMarcelo Trylesinski <marcelotryle@gmail.com>
Sun, 3 Oct 2021 15:21:38 +0000 (17:21 +0200)
committerGitHub <noreply@github.com>
Sun, 3 Oct 2021 15:21:38 +0000 (17:21 +0200)
* ðŸ”¥ Remove GraphQL support

* Remove graphene dependency and add docs

* Update docs/graphql.md

Co-authored-by: Jamie Hewland <jhewland@gmail.com>
* Update docs/graphql.md

Co-authored-by: Jamie Hewland <jhewland@gmail.com>
* Remove aiofiles warning on setup.cfg

Co-authored-by: Jamie Hewland <jhewland@gmail.com>
README.md
docs/graphql.md
docs/img/graphiql.png [deleted file]
docs/index.md
requirements.txt
setup.cfg
setup.py
starlette/graphql.py [deleted file]
tests/conftest.py
tests/test_graphql.py [deleted file]

index 8eedea9523eaa63b84783091c7682f9cf5353a00..a7d94904dfd362773ce8c4e57369d3763c49691f 100644 (file)
--- 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
index 281bdea8508dbb60352ddd34ac9d29a82ac37ad4..9b00106bb2ae3c8293c2ebdb9ebf6859014cea8c 100644 (file)
@@ -1,116 +1,9 @@
+GraphQL support in Starlette was deprecated in version 0.15.0, and removed in version 0.17.0.
 
-!!! Warning
+Although GraphQL support is no longer built in to Starlette, you can still use GraphQL with Starlette via 3rd party libraries. These libraries all have Starlette-specific guides to help you do just that:
 
-    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)
-```
diff --git a/docs/img/graphiql.png b/docs/img/graphiql.png
deleted file mode 100644 (file)
index 7851993..0000000
Binary files a/docs/img/graphiql.png and /dev/null differ
index b9692a1fbc1b47bfd94108d8fe11d3f20ee5b85b..a9ec4106f90bd7f9266ef3206c0966ebff3b3f2a 100644 (file)
@@ -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
index abc7a3b0a540045e029a8689a8e50133ff4bb880..59cc2edf7ecfd20abb6b02855de7d4b182275f00 100644 (file)
@@ -1,5 +1,4 @@
 # Optionals
-graphene; python_version<'3.10'
 itsdangerous
 jinja2
 python-multipart
index 1266d95c8e56a592e1b54013ef9b20620061d6c2..b3f84dca1e1a8838cd0d11a0741adfffa0d24b3c 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -24,8 +24,6 @@ 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
     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 +32,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
index 31789fe09dc8e62d554dc5da124a810e9844e72b..3b8d32e16b0f6d63ea2de7a2a97d924c948f9ae5 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -44,7 +44,6 @@ setup(
     ],
     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 (file)
index 6e5d6ec..0000000
+++ /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
index bb68aa5e202af6c0a6f25aa17e9c6978bcbed3a3..4f210b3bd6c498e58878458d14ca2ab4de3ac3b2 100644 (file)
@@ -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 (file)
index 8492439..0000000
+++ /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"}}