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..9b00106bb 100644 --- a/docs/graphql.md +++ b/docs/graphql.md @@ -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", "") -``` - -## 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", "") - 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 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..b3f84dca1 100644 --- 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 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 = """ - - - - - - - - - - - - - -
Loading...
- - - -""" # 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 "" 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"}}