diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6a34bba..454da04 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,17 +10,17 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.11" - name: Build wheel and source tarball run: | pip install wheel python setup.py sdist - name: Publish a Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@v1.1.0 + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 90ba2a1..58601c5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip @@ -19,4 +19,4 @@ jobs: - name: Run lint and static type checks run: tox env: - TOXENV: flake8,black,import-order,mypy,manifest + TOXENV: pre-commit,mypy diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 31616ec..ceecb95 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,22 +8,20 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] os: [ubuntu-latest, windows-latest] exclude: - - os: windows-latest - python-version: "3.6" - - os: windows-latest - python-version: "3.7" - os: windows-latest python-version: "3.8" - os: windows-latest - python-version: "3.10" + python-version: "3.9" + - os: windows-latest + python-version: "3.11" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -32,18 +30,16 @@ jobs: pip install tox tox-gh-actions - name: Test with tox run: tox - env: - TOXENV: ${{ matrix.toxenv }} coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.11" - name: Install test dependencies run: | python -m pip install --upgrade pip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..bb933c2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +default_language_version: + python: python3.11 +exclude: LICENSE +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-merge-conflict + - id: check-json + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + exclude: ^docs/.*$ + - id: pretty-format-json + args: + - --autofix + - id: trailing-whitespace +- repo: https://github.com/mgedmin/check-manifest + rev: "0.49" + hooks: + - id: check-manifest +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.2 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix, --show-fixes] + - id: ruff-format diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..228088e --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,34 @@ +select = [ + "E", # pycodestyle + "W", # pycodestyle + "F", # pyflake + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] + +ignore = [ + "E501", # line-too-long + "B904", # check for raise statements in exception handlers that lack a from clause +] + +exclude = [ + "**/docs", +] + +target-version = "py38" + +[per-file-ignores] +# Ignore unused imports (F401) in these files +"__init__.py" = ["F401"] + +[isort] +known-first-party = ["graphql_server"] +combine-as-imports = true + +[pyupgrade] +# this keeps annotation syntaxes like Union[X, Y] instead of X | Y +# to not break Python 3.8 +# https://beta.ruff.rs/docs/settings/#pyupgrade-keep-runtime-typing +keep-runtime-typing = true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 98f59f0..79d237b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,8 +31,7 @@ Activate the virtualenv and install dependencies by running: python pip install -e ".[test]" ``` -If you are using Linux or MacOS, you can make use of Makefile command -`make dev-setup`, which is a shortcut for the above python command. +If you are using Linux or MacOS, you can make use of Makefile command `make dev-setup`, which is a shortcut for the above python command. ### Development on Conda @@ -60,8 +59,7 @@ After developing, the full test suite can be evaluated by running: pytest tests --cov=graphql-server -vv ``` -If you are using Linux or MacOS, you can make use of Makefile command -`make tests`, which is a shortcut for the above python command. +If you are using Linux or MacOS, you can make use of Makefile command `make tests`, which is a shortcut for the above python command. You can also test on several python environments by using tox. @@ -73,8 +71,7 @@ Install tox: pip install tox ``` -Run `tox` on your virtualenv (do not forget to activate it!) -and that's it! +Run `tox` on your virtualenv (do not forget to activate it!) and that's it! ### Running tox on Conda @@ -89,5 +86,4 @@ This install tox underneath so no need to install it before. Then uncomment the `requires = tox-conda` line on `tox.ini` file. -Run `tox` and you will see all the environments being created -and all passing tests. :rocket: +Run `tox` and you will see all the environments being created and all passing tests. :rocket: diff --git a/MANIFEST.in b/MANIFEST.in index a6c003d..67b0e2f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,8 @@ include CONTRIBUTING.md include codecov.yml include tox.ini +include .pre-commit-config.yaml +include .ruff.toml recursive-include docs *.md *.svg diff --git a/codecov.yml b/codecov.yml index c393a12..c155caa 100644 --- a/codecov.yml +++ b/codecov.yml @@ -7,4 +7,4 @@ coverage: status: project: default: - target: auto \ No newline at end of file + target: auto diff --git a/docs/aiohttp.md b/docs/aiohttp.md index d65bcb8..b301855 100644 --- a/docs/aiohttp.md +++ b/docs/aiohttp.md @@ -47,19 +47,19 @@ gql_view(request) # <-- the instance is callable and expects a `aiohttp.web.Req ### Supported options for GraphQLView - * `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request. + * `schema`: The GraphQL schema object that you want the view to execute when it gets a valid request. Accepts either an object of type `GraphQLSchema` from `graphql-core` or `Schema` from `graphene`. For Graphene v3, passing either `schema: graphene.Schema` or `schema.graphql_schema` is allowed. * `context`: A value to pass as the `context_value` to graphql `execute` function. By default is set to `dict` with request object at key `request`. * `root_value`: The `root_value` you want to provide to graphql `execute`. * `pretty`: Whether or not you want the response to be pretty printed JSON. * `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). - * `graphiql_version`: The graphiql version to load. Defaults to **"1.0.3"**. + * `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**. * `graphiql_template`: Inject a Jinja template string to customize GraphiQL. * `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**. - * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses -`Template.render_async` instead of `Template.render`. If environment is not set, fallbacks to simple regex-based renderer. + * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses `Template.render_async` instead of `Template.render`. If environment is not set, fallbacks to simple regex-based renderer. * `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) * `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/). * `validation_rules`: A list of graphql validation rules. + * `execution_context_class`: Specifies a custom execution context class. * `max_age`: Sets the response header Access-Control-Max-Age for preflight requests. * `encode`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`). * `format_error`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`. diff --git a/docs/flask.md b/docs/flask.md index f3a36e7..dfe0aa7 100644 --- a/docs/flask.md +++ b/docs/flask.md @@ -39,26 +39,21 @@ if __name__ == '__main__': This will add `/graphql` endpoint to your app and enable the GraphiQL IDE. -### Special Note for Graphene v3 - -If you are using the `Schema` type of [Graphene](https://github.com/graphql-python/graphene) library, be sure to use the `graphql_schema` attribute to pass as schema on the `GraphQLView` view. Otherwise, the `GraphQLSchema` from `graphql-core` is the way to go. - -More info at [Graphene v3 release notes](https://github.com/graphql-python/graphene/wiki/v3-release-notes#graphene-schema-no-longer-subclasses-graphqlschema-type) and [GraphQL-core 3 usage](https://github.com/graphql-python/graphql-core#usage). - - ### Supported options for GraphQLView - * `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request. + * `schema`: The GraphQL schema object that you want the view to execute when it gets a valid request. Accepts either an object of type `GraphQLSchema` from `graphql-core` or `Schema` from `graphene`. For Graphene v3, passing either `schema: graphene.Schema` or `schema.graphql_schema` is allowed. * `context`: A value to pass as the `context_value` to graphql `execute` function. By default is set to `dict` with request object at key `request`. * `root_value`: The `root_value` you want to provide to graphql `execute`. * `pretty`: Whether or not you want the response to be pretty printed JSON. * `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). - * `graphiql_version`: The graphiql version to load. Defaults to **"1.0.3"**. + * `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**. * `graphiql_template`: Inject a Jinja template string to customize GraphiQL. * `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**. + * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If environment is not set, fallbacks to simple regex-based renderer. * `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) * `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/). - * `validation_rules`: A list of graphql validation rules. + * `validation_rules`: A list of graphql validation rules. + * `execution_context_class`: Specifies a custom execution context class. * `encode`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`). * `format_error`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`. * `subscriptions`: The GraphiQL socket endpoint for using subscriptions in graphql-ws. @@ -79,4 +74,4 @@ class UserRootValue(GraphQLView): ``` ## Contributing -See [CONTRIBUTING.md](../CONTRIBUTING.md) \ No newline at end of file +See [CONTRIBUTING.md](../CONTRIBUTING.md) diff --git a/docs/sanic.md b/docs/sanic.md index e922598..102e38d 100644 --- a/docs/sanic.md +++ b/docs/sanic.md @@ -39,19 +39,19 @@ This will add `/graphql` endpoint to your app and enable the GraphiQL IDE. ### Supported options for GraphQLView - * `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request. + * `schema`: The GraphQL schema object that you want the view to execute when it gets a valid request. Accepts either an object of type `GraphQLSchema` from `graphql-core` or `Schema` from `graphene`. For Graphene v3, passing either `schema: graphene.Schema` or `schema.graphql_schema` is allowed. * `context`: A value to pass as the `context_value` to graphql `execute` function. By default is set to `dict` with request object at key `request`. * `root_value`: The `root_value` you want to provide to graphql `execute`. * `pretty`: Whether or not you want the response to be pretty printed JSON. * `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). - * `graphiql_version`: The graphiql version to load. Defaults to **"1.0.3"**. + * `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**. * `graphiql_template`: Inject a Jinja template string to customize GraphiQL. * `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**. - * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses -`Template.render_async` instead of `Template.render`. If environment is not set, fallbacks to simple regex-based renderer. + * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses `Template.render_async` instead of `Template.render`. If environment is not set, fallbacks to simple regex-based renderer. * `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) * `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/). - * `validation_rules`: A list of graphql validation rules. + * `validation_rules`: A list of graphql validation rules. + * `execution_context_class`: Specifies a custom execution context class. * `max_age`: Sets the response header Access-Control-Max-Age for preflight requests. * `encode`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`). * `format_error`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`. @@ -72,4 +72,4 @@ class UserRootValue(GraphQLView): ``` ## Contributing -See [CONTRIBUTING.md](../CONTRIBUTING.md) \ No newline at end of file +See [CONTRIBUTING.md](../CONTRIBUTING.md) diff --git a/docs/webob.md b/docs/webob.md index 41c0ad1..2f88a31 100644 --- a/docs/webob.md +++ b/docs/webob.md @@ -38,17 +38,19 @@ This will add `/graphql` endpoint to your app and enable the GraphiQL IDE. ### Supported options for GraphQLView - * `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request. + * `schema`: The GraphQL schema object that you want the view to execute when it gets a valid request. Accepts either an object of type `GraphQLSchema` from `graphql-core` or `Schema` from `graphene`. For Graphene v3, passing either `schema: graphene.Schema` or `schema.graphql_schema` is allowed. * `context`: A value to pass as the `context_value` to graphql `execute` function. By default is set to `dict` with request object at key `request`. * `root_value`: The `root_value` you want to provide to graphql `execute`. * `pretty`: Whether or not you want the response to be pretty printed JSON. * `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). - * `graphiql_version`: The graphiql version to load. Defaults to **"1.0.3"**. + * `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**. * `graphiql_template`: Inject a Jinja template string to customize GraphiQL. * `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**. + * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If environment is not set, fallbacks to simple regex-based renderer. * `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) * `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/). - * `validation_rules`: A list of graphql validation rules. + * `validation_rules`: A list of graphql validation rules. + * `execution_context_class`: Specifies a custom execution context class. * `encode`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`). * `format_error`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`. * `enable_async`: whether `async` mode will be enabled. @@ -59,4 +61,4 @@ This will add `/graphql` endpoint to your app and enable the GraphiQL IDE. * `should_persist_headers`: An optional boolean which enables to persist headers to storage when true. Defaults to **false**. ## Contributing -See [CONTRIBUTING.md](../CONTRIBUTING.md) \ No newline at end of file +See [CONTRIBUTING.md](../CONTRIBUTING.md) diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index ee54cdb..f8456de 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -9,17 +9,7 @@ import json from collections import namedtuple from collections.abc import MutableMapping -from typing import ( - Any, - Callable, - Collection, - Dict, - List, - Optional, - Type, - Union, - cast, -) +from typing import Any, Callable, Collection, Dict, List, Optional, Type, Union, cast from graphql.error import GraphQLError from graphql.execution import ExecutionResult, execute @@ -345,3 +335,17 @@ def format_execution_result( response = {"data": execution_result.data} return FormattedResult(response, status_code) + + +def _check_jinja(jinja_env: Any) -> None: + try: + from jinja2 import Environment + except ImportError: # pragma: no cover + raise RuntimeError( + "Attempt to set 'jinja_env' to a value other than None while Jinja2 is not installed.\n" + "Please install Jinja2 to render GraphiQL with Jinja2.\n" + "Otherwise set 'jinja_env' to None to use the simple regex renderer." + ) + + if not isinstance(jinja_env, Environment): # pragma: no cover + raise TypeError("'jinja_env' has to be of type jinja2.Environment.") diff --git a/graphql_server/aiohttp/graphqlview.py b/graphql_server/aiohttp/graphqlview.py index d98becd..ea23037 100644 --- a/graphql_server/aiohttp/graphqlview.py +++ b/graphql_server/aiohttp/graphqlview.py @@ -1,15 +1,18 @@ +import asyncio import copy from collections.abc import MutableMapping from functools import partial from typing import List from aiohttp import web -from graphql import ExecutionResult, GraphQLError, specified_rules +from graphql import GraphQLError, specified_rules +from graphql.pyutils import is_awaitable from graphql.type.schema import GraphQLSchema from graphql_server import ( GraphQLParams, HttpQueryError, + _check_jinja, encode_execution_results, format_error_default, json_encode, @@ -22,6 +25,7 @@ GraphiQLOptions, render_graphiql_async, ) +from graphql_server.utils import wrap_in_async class GraphQLView: @@ -35,6 +39,7 @@ class GraphQLView: graphiql_html_title = None middleware = None validation_rules = None + execution_context_class = None batch = False jinja_env = None max_age = 86400 @@ -51,7 +56,7 @@ class GraphQLView: encode = staticmethod(json_encode) def __init__(self, **kwargs): - super(GraphQLView, self).__init__() + super().__init__() for key, value in kwargs.items(): if hasattr(self, key): setattr(self, key, value) @@ -62,13 +67,16 @@ def __init__(self, **kwargs): if not isinstance(self.schema, GraphQLSchema): raise TypeError("A Schema is required to be provided to GraphQLView.") + if self.jinja_env is not None: + _check_jinja(self.jinja_env) + def get_root_value(self): return self.root_value def get_context(self, request): context = ( copy.copy(self.context) - if self.context and isinstance(self.context, MutableMapping) + if self.context is not None and isinstance(self.context, MutableMapping) else {} ) if isinstance(context, MutableMapping) and "request" not in context: @@ -83,6 +91,9 @@ def get_validation_rules(self): return specified_rules return self.validation_rules + def get_execution_context_class(self): + return self.execution_context_class + @staticmethod async def parse_body(request): content_type = request.content_type @@ -158,13 +169,18 @@ async def __call__(self, request): context_value=self.get_context(request), middleware=self.get_middleware(), validation_rules=self.get_validation_rules(), + execution_context_class=self.get_execution_context_class(), ) exec_res = ( - [ - ex if ex is None or isinstance(ex, ExecutionResult) else await ex - for ex in execution_results - ] + await asyncio.gather( + *( + ex + if ex is not None and is_awaitable(ex) + else wrap_in_async(lambda x: x)(ex) + for ex in execution_results + ) + ) if self.enable_async else execution_results ) @@ -172,15 +188,15 @@ async def __call__(self, request): exec_res, is_batch=isinstance(data, list), format_error=self.format_error, - encode=partial(self.encode, pretty=is_pretty), # noqa: ignore + encode=partial(self.encode, pretty=is_pretty), ) if is_graphiql: graphiql_data = GraphiQLData( result=result, - query=getattr(all_params[0], "query"), - variables=getattr(all_params[0], "variables"), - operation_name=getattr(all_params[0], "operation_name"), + query=all_params[0].query, + variables=all_params[0].variables, + operation_name=all_params[0].operation_name, subscription_url=self.subscriptions, headers=self.headers, ) @@ -209,7 +225,7 @@ async def __call__(self, request): except HttpQueryError as err: parsed_error = GraphQLError(err.message) return web.Response( - body=self.encode(dict(errors=[self.format_error(parsed_error)])), + body=self.encode({"errors": [self.format_error(parsed_error)]}), status=err.status_code, headers=err.headers, content_type="application/json", diff --git a/graphql_server/error.py b/graphql_server/error.py index b0ca74a..497f121 100644 --- a/graphql_server/error.py +++ b/graphql_server/error.py @@ -16,7 +16,7 @@ def __init__(self, status_code, message=None, is_graphql_error=False, headers=No self.message = message self.is_graphql_error = is_graphql_error self.headers = headers - super(HttpQueryError, self).__init__(message) + super().__init__(message) def __eq__(self, other): """Check whether this HTTP query error is equal to another one.""" diff --git a/graphql_server/flask/graphqlview.py b/graphql_server/flask/graphqlview.py index 063a67a..d59124d 100644 --- a/graphql_server/flask/graphqlview.py +++ b/graphql_server/flask/graphqlview.py @@ -12,6 +12,7 @@ from graphql_server import ( GraphQLParams, HttpQueryError, + _check_jinja, encode_execution_results, format_error_default, json_encode, @@ -37,7 +38,9 @@ class GraphQLView(View): graphiql_html_title = None middleware = None validation_rules = None + execution_context_class = None batch = False + jinja_env = None subscriptions = None headers = None default_query = None @@ -50,7 +53,7 @@ class GraphQLView(View): encode = staticmethod(json_encode) def __init__(self, **kwargs): - super(GraphQLView, self).__init__() + super().__init__() for key, value in kwargs.items(): if hasattr(self, key): setattr(self, key, value) @@ -61,13 +64,16 @@ def __init__(self, **kwargs): if not isinstance(self.schema, GraphQLSchema): raise TypeError("A Schema is required to be provided to GraphQLView.") + if self.jinja_env is not None: + _check_jinja(self.jinja_env) + def get_root_value(self): return self.root_value def get_context(self): context = ( copy.copy(self.context) - if self.context and isinstance(self.context, MutableMapping) + if self.context is not None and isinstance(self.context, MutableMapping) else {} ) if isinstance(context, MutableMapping) and "request" not in context: @@ -82,6 +88,9 @@ def get_validation_rules(self): return specified_rules return self.validation_rules + def get_execution_context_class(self): + return self.execution_context_class + def dispatch_request(self): try: request_method = request.method.lower() @@ -105,20 +114,21 @@ def dispatch_request(self): context_value=self.get_context(), middleware=self.get_middleware(), validation_rules=self.get_validation_rules(), + execution_context_class=self.get_execution_context_class(), ) result, status_code = encode_execution_results( execution_results, is_batch=isinstance(data, list), format_error=self.format_error, - encode=partial(self.encode, pretty=pretty), # noqa + encode=partial(self.encode, pretty=pretty), ) if show_graphiql: graphiql_data = GraphiQLData( result=result, - query=getattr(all_params[0], "query"), - variables=getattr(all_params[0], "variables"), - operation_name=getattr(all_params[0], "operation_name"), + query=all_params[0].query, + variables=all_params[0].variables, + operation_name=all_params[0].operation_name, subscription_url=self.subscriptions, headers=self.headers, ) @@ -126,7 +136,7 @@ def dispatch_request(self): graphiql_version=self.graphiql_version, graphiql_template=self.graphiql_template, graphiql_html_title=self.graphiql_html_title, - jinja_env=None, + jinja_env=self.jinja_env, ) graphiql_options = GraphiQLOptions( default_query=self.default_query, @@ -143,7 +153,7 @@ def dispatch_request(self): except HttpQueryError as e: parsed_error = GraphQLError(e.message) return Response( - self.encode(dict(errors=[self.format_error(parsed_error)])), + self.encode({"errors": [self.format_error(parsed_error)]}), status=e.status_code, headers=e.headers, content_type="application/json", diff --git a/graphql_server/quart/graphqlview.py b/graphql_server/quart/graphqlview.py index ff737ec..8885e5e 100644 --- a/graphql_server/quart/graphqlview.py +++ b/graphql_server/quart/graphqlview.py @@ -1,11 +1,12 @@ +import asyncio import copy -import sys from collections.abc import MutableMapping from functools import partial from typing import List -from graphql import ExecutionResult, specified_rules +from graphql import specified_rules from graphql.error import GraphQLError +from graphql.pyutils import is_awaitable from graphql.type.schema import GraphQLSchema from quart import Response, render_template_string, request from quart.views import View @@ -13,6 +14,7 @@ from graphql_server import ( GraphQLParams, HttpQueryError, + _check_jinja, encode_execution_results, format_error_default, json_encode, @@ -25,6 +27,7 @@ GraphiQLOptions, render_graphiql_sync, ) +from graphql_server.utils import wrap_in_async class GraphQLView(View): @@ -38,7 +41,9 @@ class GraphQLView(View): graphiql_html_title = None middleware = None validation_rules = None + execution_context_class = None batch = False + jinja_env = None enable_async = False subscriptions = None headers = None @@ -52,7 +57,7 @@ class GraphQLView(View): encode = staticmethod(json_encode) def __init__(self, **kwargs): - super(GraphQLView, self).__init__() + super().__init__() for key, value in kwargs.items(): if hasattr(self, key): setattr(self, key, value) @@ -63,13 +68,16 @@ def __init__(self, **kwargs): if not isinstance(self.schema, GraphQLSchema): raise TypeError("A Schema is required to be provided to GraphQLView.") + if self.jinja_env is not None: + _check_jinja(self.jinja_env) + def get_root_value(self): return self.root_value def get_context(self): context = ( copy.copy(self.context) - if self.context and isinstance(self.context, MutableMapping) + if self.context is not None and isinstance(self.context, MutableMapping) else {} ) if isinstance(context, MutableMapping) and "request" not in context: @@ -84,6 +92,9 @@ def get_validation_rules(self): return specified_rules return self.validation_rules + def get_execution_context_class(self): + return self.execution_context_class + async def dispatch_request(self): try: request_method = request.method.lower() @@ -107,12 +118,17 @@ async def dispatch_request(self): context_value=self.get_context(), middleware=self.get_middleware(), validation_rules=self.get_validation_rules(), + execution_context_class=self.get_execution_context_class(), ) exec_res = ( - [ - ex if ex is None or isinstance(ex, ExecutionResult) else await ex - for ex in execution_results - ] + await asyncio.gather( + *( + ex + if ex is not None and is_awaitable(ex) + else wrap_in_async(lambda x: x)(ex) + for ex in execution_results + ) + ) if self.enable_async else execution_results ) @@ -120,15 +136,15 @@ async def dispatch_request(self): exec_res, is_batch=isinstance(data, list), format_error=self.format_error, - encode=partial(self.encode, pretty=pretty), # noqa + encode=partial(self.encode, pretty=pretty), ) if show_graphiql: graphiql_data = GraphiQLData( result=result, - query=getattr(all_params[0], "query"), - variables=getattr(all_params[0], "variables"), - operation_name=getattr(all_params[0], "operation_name"), + query=all_params[0].query, + variables=all_params[0].variables, + operation_name=all_params[0].operation_name, subscription_url=self.subscriptions, headers=self.headers, ) @@ -136,7 +152,7 @@ async def dispatch_request(self): graphiql_version=self.graphiql_version, graphiql_template=self.graphiql_template, graphiql_html_title=self.graphiql_html_title, - jinja_env=None, + jinja_env=self.jinja_env, ) graphiql_options = GraphiQLOptions( default_query=self.default_query, @@ -153,7 +169,7 @@ async def dispatch_request(self): except HttpQueryError as e: parsed_error = GraphQLError(e.message) return Response( - self.encode(dict(errors=[self.format_error(parsed_error)])), + self.encode({"errors": [self.format_error(parsed_error)]}), status=e.status_code, headers=e.headers, content_type="application/json", @@ -165,11 +181,11 @@ async def parse_body(): # information provided by content_type content_type = request.mimetype if content_type == "application/graphql": - refined_data = await request.get_data(raw=False) + refined_data = await request.get_data(as_text=True) return {"query": refined_data} elif content_type == "application/json": - refined_data = await request.get_data(raw=False) + refined_data = await request.get_data(as_text=True) return load_json_body(refined_data) elif content_type == "application/x-www-form-urlencoded": @@ -191,20 +207,8 @@ def should_display_graphiql(self): def request_wants_html(): best = request.accept_mimetypes.best_match(["application/json", "text/html"]) - # Needed as this was introduced at Quart 0.8.0: https://gitlab.com/pgjones/quart/-/issues/189 - def _quality(accept, key: str) -> float: - for option in accept.options: - if accept._values_match(key, option.value): - return option.quality - return 0.0 - - if sys.version_info >= (3, 7): - return ( - best == "text/html" - and request.accept_mimetypes[best] - > request.accept_mimetypes["application/json"] - ) - else: - return best == "text/html" and _quality( - request.accept_mimetypes, best - ) > _quality(request.accept_mimetypes, "application/json") + return ( + best == "text/html" + and request.accept_mimetypes[best] + > request.accept_mimetypes["application/json"] + ) diff --git a/graphql_server/render_graphiql.py b/graphql_server/render_graphiql.py index c942300..0da06b9 100644 --- a/graphql_server/render_graphiql.py +++ b/graphql_server/render_graphiql.py @@ -1,13 +1,19 @@ -"""Based on (express-graphql)[https://github.com/graphql/express-graphql/blob/master/src/renderGraphiQL.js] and -(subscriptions-transport-ws)[https://github.com/apollographql/subscriptions-transport-ws]""" +"""Based on (express-graphql)[https://github.com/graphql/express-graphql/blob/main/src/renderGraphiQL.ts] and +(graphql-ws)[https://github.com/enisdenjo/graphql-ws]""" import json import re from typing import Any, Dict, Optional, Tuple -from jinja2 import Environment +# This Environment import is only for type checking purpose, +# and only relevant if rendering GraphiQL with Jinja +try: + from jinja2 import Environment +except ImportError: # pragma: no cover + pass + from typing_extensions import TypedDict -GRAPHIQL_VERSION = "1.0.3" +GRAPHIQL_VERSION = "2.2.0" GRAPHIQL_TEMPLATE = """

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy