From a62cee751fe304628bd7ff53c060d67371bab122 Mon Sep 17 00:00:00 2001 From: Jean-Louis Fuchs Date: Wed, 29 Apr 2020 13:36:11 +0200 Subject: [PATCH 01/13] Update dependencies to graph* 3.0 * Update mentions of django 1.11 and 2.0 --- .github/workflows/tests.yml | 2 +- README.md | 2 +- README.rst | 2 +- docs/authorization.rst | 11 +---------- docs/installation.rst | 16 ++-------------- setup.py | 9 +++++---- tox.ini | 4 +--- 7 files changed, 12 insertions(+), 34 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 11dcd98fa..270b24e61 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ jobs: strategy: max-parallel: 4 matrix: - django: ["1.11", "2.2", "3.0"] + django: ["2.2", "3.0"] python-version: ["3.6", "3.7", "3.8"] steps: diff --git a/README.md b/README.md index 86050659a..249020997 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ A [Django](https://www.djangoproject.com/) integration for [Graphene](http://gra For installing graphene, just run this command in your shell ```bash -pip install "graphene-django>=2.0" +pip install "graphene-django>=3" ``` ### Settings diff --git a/README.rst b/README.rst index 44feaee83..4ac7dda29 100644 --- a/README.rst +++ b/README.rst @@ -23,7 +23,7 @@ For installing graphene, just run this command in your shell .. code:: bash - pip install "graphene-django>=2.0" + pip install "graphene-django>=3" Settings ~~~~~~~~ diff --git a/docs/authorization.rst b/docs/authorization.rst index 7e09c3781..8ef05b48c 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -166,16 +166,7 @@ To restrict users from accessing the GraphQL API page the standard Django LoginR After this, you can use the new ``PrivateGraphQLView`` in the project's URL Configuration file ``url.py``: -For Django 1.11: - -.. code:: python - - urlpatterns = [ - # some other urls - url(r'^graphql$', PrivateGraphQLView.as_view(graphiql=True, schema=schema)), - ] - -For Django 2.0 and above: +For Django 2.2 and above: .. code:: python diff --git a/docs/installation.rst b/docs/installation.rst index 048a9942d..573032e63 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -8,7 +8,7 @@ Requirements Graphene-Django currently supports the following versions of Django: -* >= Django 1.11 +* >= Django 2.2 Installation ------------ @@ -32,19 +32,7 @@ Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of We need to add a ``graphql`` URL to the ``urls.py`` of your Django project: -For Django 1.11: - -.. code:: python - - from django.conf.urls import url - from graphene_django.views import GraphQLView - - urlpatterns = [ - # ... - url(r"graphql", GraphQLView.as_view(graphiql=True)), - ] - -For Django 2.0 and above: +For Django 2.2 and above: .. code:: python diff --git a/setup.py b/setup.py index 7b0da550a..980871a6b 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ -from setuptools import find_packages, setup import ast import re +from setuptools import find_packages, setup + _version_re = re.compile(r"__version__\s+=\s+(.*)") with open("graphene_django/__init__.py", "rb") as f: @@ -53,9 +54,9 @@ keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests"]), install_requires=[ - "graphene>=2.1.7,<3", - "graphql-core>=2.1.0,<3", - "Django>=1.11,!=2.0.*,!=2.1.*", + "graphene>=3.0.0b1,<4", + "graphql-core>=3.1.0,<4", + "Django>=2.2", "promise>=2.1", ], setup_requires=["pytest-runner"], diff --git a/tox.ini b/tox.ini index 8e0163215..7e01ac922 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{36,37,38}-django{111,22,30,master}, + py{36,37,38}-django{22,30,master}, black,flake8 [gh-actions] @@ -11,7 +11,6 @@ python = [gh-actions:env] DJANGO = - 1.11: django111 2.2: django22 3.0: django30 master: djangomaster @@ -24,7 +23,6 @@ setenv = deps = -e.[test] psycopg2-binary - django111: Django>=1.11,<2.0 django22: Django>=2.2,<3.0 django30: Django>=3.0a1,<3.1 djangomaster: https://github.com/django/django/archive/master.zip From a0e31c5be66edc72051386d8c8a5eae66696d460 Mon Sep 17 00:00:00 2001 From: Tomasz Kontusz Date: Sun, 15 Sep 2019 16:31:12 +0200 Subject: [PATCH 02/13] Use adapters instead of Connection and PageInfo --- graphene_django/fields.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index f0a382814..a4479d5ce 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,12 +1,11 @@ from functools import partial from django.db.models.query import QuerySet -from graphql_relay.connection.arrayconnection import connection_from_list_slice -from promise import Promise - from graphene import NonNull -from graphene.relay import ConnectionField, PageInfo +from graphene.relay import ConnectionField from graphene.types import Field, List +from graphql_relay.connection.arrayconnection import connection_from_list_slice +from promise import Promise from .settings import graphene_settings from .utils import maybe_queryset @@ -128,9 +127,9 @@ def resolve_connection(cls, connection, args, iterable): slice_start=0, list_length=_len, list_slice_length=_len, - connection_type=connection, + connection_type=partial(connection_adapter, connection), edge_type=connection.Edge, - pageinfo_type=PageInfo, + pageinfo_type=page_info_adapter, ) connection.iterable = iterable connection.length = _len From 1a4e30af39982ff04769b58ca918aab00e60bcda Mon Sep 17 00:00:00 2001 From: Tomasz Kontusz Date: Sun, 15 Sep 2019 16:50:35 +0200 Subject: [PATCH 03/13] Don't use __debug for DjangoDebug Fields starting with __ are reserved for introspection --- graphene_django/debug/tests/test_query.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/graphene_django/debug/tests/test_query.py b/graphene_django/debug/tests/test_query.py index 7226f9b8a..7255ec69e 100644 --- a/graphene_django/debug/tests/test_query.py +++ b/graphene_django/debug/tests/test_query.py @@ -68,7 +68,7 @@ class Meta: class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) - debug = graphene.Field(DjangoDebug, name="__debug") + debug = graphene.Field(DjangoDebug, name="_debug") def resolve_reporter(self, info, **args): return Reporter.objects.first() @@ -82,7 +82,7 @@ def resolve_reporter(self, info, **args): pets { edges { node { lastName } } } } } } } - __debug { + _debug { sql { rawSql } @@ -110,12 +110,12 @@ def resolve_reporter(self, info, **args): ) assert not result.errors query = str(Reporter.objects.order_by("pk")[:1].query) - assert result.data["__debug"]["sql"][0]["rawSql"] == query - assert "COUNT" in result.data["__debug"]["sql"][1]["rawSql"] - assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"] - assert "COUNT" in result.data["__debug"]["sql"][3]["rawSql"] - assert "tests_reporter_pets" in result.data["__debug"]["sql"][4]["rawSql"] - assert len(result.data["__debug"]["sql"]) == 5 + assert result.data["_debug"]["sql"][0]["rawSql"] == query + assert "COUNT" in result.data["_debug"]["sql"][1]["rawSql"] + assert "tests_reporter_pets" in result.data["_debug"]["sql"][2]["rawSql"] + assert "COUNT" in result.data["_debug"]["sql"][3]["rawSql"] + assert "tests_reporter_pets" in result.data["_debug"]["sql"][4]["rawSql"] + assert len(result.data["_debug"]["sql"]) == 5 assert result.data["reporter"] == expected["reporter"] From 51fe6d06a9a5bcb692ca3bedc2a40aed8889f1eb Mon Sep 17 00:00:00 2001 From: Tomasz Kontusz Date: Sun, 15 Sep 2019 16:51:32 +0200 Subject: [PATCH 04/13] Fix test collection --- graphene_django/converter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 36116ed5a..f5e890b7a 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -23,7 +23,7 @@ ) from graphene.types.json import JSONString from graphene.utils.str_converters import to_camel_case, to_const -from graphql import assert_valid_name +from graphql import assert_valid_name, GraphQLError from .settings import graphene_settings from .compat import ArrayField, HStoreField, JSONField, RangeField @@ -34,7 +34,7 @@ def convert_choice_name(name): name = to_const(force_str(name)) try: assert_valid_name(name) - except AssertionError: + except GraphQLError: name = "A_%s" % name return name @@ -52,7 +52,7 @@ def get_choices(choices): while name in converted_names: name += "_" + str(len(converted_names)) converted_names.append(name) - description = help_text + description = str(help_text) # TODO: translatable description: https://github.com/graphql-python/graphql-core-next/issues/58 yield name, value, description From b63fd1cd7c3fda88ce71729c70c5e558b5484e74 Mon Sep 17 00:00:00 2001 From: Tomasz Kontusz Date: Sun, 15 Sep 2019 18:30:00 +0200 Subject: [PATCH 05/13] Fix asserts for errors that come back with visualised location --- graphene_django/converter.py | 4 +++- graphene_django/tests/test_query.py | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index f5e890b7a..ffd3d9483 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -52,7 +52,9 @@ def get_choices(choices): while name in converted_names: name += "_" + str(len(converted_names)) converted_names.append(name) - description = str(help_text) # TODO: translatable description: https://github.com/graphql-python/graphql-core-next/issues/58 + description = str( + help_text + ) # TODO: translatable description: https://github.com/graphql-python/graphql-core-next/issues/58 yield name, value, description diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 698ca2388..97cdc56b5 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -612,9 +612,9 @@ class Query(graphene.ObjectType): result = schema.execute(query) assert len(result.errors) == 1 - assert str(result.errors[0]) == ( + assert str(result.errors[0]).startswith( "You must provide a `first` or `last` value to properly " - "paginate the `allReporters` connection." + "paginate the `allReporters` connection.\n" ) assert result.data == expected @@ -653,9 +653,9 @@ class Query(graphene.ObjectType): result = schema.execute(query) assert len(result.errors) == 1 - assert str(result.errors[0]) == ( + assert str(result.errors[0]).startswith( "Requesting 101 records on the `allReporters` connection " - "exceeds the `first` limit of 100 records." + "exceeds the `first` limit of 100 records.\n" ) assert result.data == expected @@ -694,9 +694,9 @@ class Query(graphene.ObjectType): result = schema.execute(query) assert len(result.errors) == 1 - assert str(result.errors[0]) == ( + assert str(result.errors[0]).startswith( "Requesting 101 records on the `allReporters` connection " - "exceeds the `last` limit of 100 records." + "exceeds the `last` limit of 100 records.\n" ) assert result.data == expected From 8f0994e65b2f78fd9c376e62bd3962d9a46bf0a8 Mon Sep 17 00:00:00 2001 From: Tomasz Kontusz Date: Sun, 15 Sep 2019 18:44:51 +0200 Subject: [PATCH 06/13] Fix test_should_preserve_prefetch_related Connection resolvers have access to pagination arguments. --- graphene_django/tests/test_query.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 97cdc56b5..a5238bf8e 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1075,7 +1075,7 @@ class Meta: class Query(graphene.ObjectType): films = DjangoConnectionField(FilmType) - def resolve_films(root, info): + def resolve_films(root, info, **args): qs = Film.objects.prefetch_related("reporters") return qs @@ -1107,7 +1107,7 @@ def resolve_films(root, info): schema = graphene.Schema(query=Query) with django_assert_num_queries(3) as captured: result = schema.execute(query) - assert not result.errors + assert not result.errors def test_should_preserve_annotations(): @@ -1160,3 +1160,4 @@ def resolve_films(root, info): } } assert result.data == expected, str(result.data) + assert not result.errors From 5a662c9d03bde83ea7a71b11d0326322fb33bd9e Mon Sep 17 00:00:00 2001 From: Tomasz Kontusz Date: Sun, 15 Sep 2019 19:44:09 +0200 Subject: [PATCH 07/13] Fix rest_framework tests --- graphene_django/rest_framework/tests/test_mutation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index 1599fead3..c2a39478b 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -17,6 +17,7 @@ def mock_info(): None, None, None, + path=None, schema=None, fragments=None, root_value=None, From 10269267a471cdbceaf24670500c63846acf70e1 Mon Sep 17 00:00:00 2001 From: Tomasz Kontusz Date: Sun, 15 Sep 2019 22:07:41 +0200 Subject: [PATCH 08/13] Port GraphQLView to Graphene 3 --- graphene_django/views.py | 68 ++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/graphene_django/views.py b/graphene_django/views.py index 8d57d5088..112447e7c 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -6,15 +6,16 @@ from django.http.response import HttpResponseBadRequest from django.shortcuts import render from django.utils.decorators import method_decorator -from django.views.generic import View from django.views.decorators.csrf import ensure_csrf_cookie - -from graphql import get_default_backend -from graphql.error import format_error as format_graphql_error +from django.views.generic import View +from graphql import OperationType, execute, get_operation_ast, parse, validate from graphql.error import GraphQLError +from graphql.error import format_error as format_graphql_error from graphql.execution import ExecutionResult from graphql.type.schema import GraphQLSchema +from graphene import Schema + from .settings import graphene_settings @@ -56,8 +57,6 @@ class GraphQLView(View): schema = None graphiql = False - executor = None - backend = None middleware = None root_value = None pretty = False @@ -66,35 +65,28 @@ class GraphQLView(View): def __init__( self, schema=None, - executor=None, middleware=None, root_value=None, graphiql=False, pretty=False, batch=False, - backend=None, ): if not schema: schema = graphene_settings.SCHEMA - if backend is None: - backend = get_default_backend() - if middleware is None: middleware = graphene_settings.MIDDLEWARE self.schema = self.schema or schema if middleware is not None: self.middleware = list(instantiate_middleware(middleware)) - self.executor = executor self.root_value = root_value self.pretty = self.pretty or pretty self.graphiql = self.graphiql or graphiql self.batch = self.batch or batch - self.backend = backend assert isinstance( - self.schema, GraphQLSchema + self.schema, Schema ), "A Schema is required to be provided to GraphQLView." assert not all((graphiql, batch)), "Use either graphiql or batch processing" @@ -108,9 +100,6 @@ def get_middleware(self, request): def get_context(self, request): return request - def get_backend(self, request): - return self.backend - @method_decorator(ensure_csrf_cookie) def dispatch(self, request, *args, **kwargs): try: @@ -172,7 +161,9 @@ def get_response(self, request, data, show_graphiql=False): self.format_error(e) for e in execution_result.errors ] - if execution_result.invalid: + if execution_result.errors and any( + not e.path for e in execution_result.errors + ): status_code = 400 else: response["data"] = execution_result.data @@ -245,14 +236,13 @@ def execute_graphql_request( raise HttpError(HttpResponseBadRequest("Must provide query string.")) try: - backend = self.get_backend(request) - document = backend.document_from_string(self.schema, query) + document = parse(query) except Exception as e: - return ExecutionResult(errors=[e], invalid=True) + return ExecutionResult(errors=[e]) if request.method.lower() == "get": - operation_type = document.get_operation_type(operation_name) - if operation_type and operation_type != "query": + operation_ast = get_operation_ast(document, operation_name) + if operation_ast and operation_ast.operation != OperationType.QUERY: if show_graphiql: return None @@ -260,28 +250,24 @@ def execute_graphql_request( HttpResponseNotAllowed( ["POST"], "Can only perform a {} operation from a POST request.".format( - operation_type + operation_ast.operation.value ), ) ) - try: - extra_options = {} - if self.executor: - # We only include it optionally since - # executor is not a valid argument in all backends - extra_options["executor"] = self.executor - - return document.execute( - root_value=self.get_root_value(request), - variable_values=variables, - operation_name=operation_name, - context_value=self.get_context(request), - middleware=self.get_middleware(request), - **extra_options - ) - except Exception as e: - return ExecutionResult(errors=[e], invalid=True) + validation_errors = validate(self.schema.graphql_schema, document) + if validation_errors: + return ExecutionResult(data=None, errors=validation_errors) + + return execute( + schema=self.schema.graphql_schema, + document=document, + root_value=self.get_root_value(request), + variable_values=variables, + operation_name=operation_name, + context_value=self.get_context(request), + middleware=self.get_middleware(request), + ) @classmethod def can_display_graphiql(cls, request, data): From 758f3263759f3253cb42cc0d764f56d3da1d36d2 Mon Sep 17 00:00:00 2001 From: Tomasz Kontusz Date: Sun, 15 Sep 2019 22:11:09 +0200 Subject: [PATCH 09/13] Fix GraphQLView tests, skip some of them Some of the tests show the problem described here: https://github.com/graphql-python/graphql-core-next/issues/61 --- graphene_django/tests/test_views.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/graphene_django/tests/test_views.py b/graphene_django/tests/test_views.py index db6cc4e80..5be94f1f3 100644 --- a/graphene_django/tests/test_views.py +++ b/graphene_django/tests/test_views.py @@ -92,6 +92,9 @@ def test_allows_get_with_operation_name(client): } +@pytest.mark.xfail( + reason="SourceLocation serialization problem: https://github.com/graphql-python/graphql-core-next/issues/61" +) def test_reports_validation_errors(client): response = client.get(url_string(query="{ test, unknownOne, unknownTwo }")) @@ -99,12 +102,14 @@ def test_reports_validation_errors(client): assert response_json(response) == { "errors": [ { - "message": 'Cannot query field "unknownOne" on type "QueryRoot".', + "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], + "path": None, }, { - "message": 'Cannot query field "unknownTwo" on type "QueryRoot".', + "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 21}], + "path": None, }, ] } @@ -124,7 +129,9 @@ def test_errors_when_missing_operation_name(client): assert response_json(response) == { "errors": [ { - "message": "Must provide operation name if query contains multiple operations." + "locations": None, + "message": "Must provide operation name if query contains multiple operations.", + "path": None, } ] } @@ -442,6 +449,9 @@ def test_supports_pretty_printing_by_request(client): ) +@pytest.mark.xfail( + reason="SourceLocation serialization problem: https://github.com/graphql-python/graphql-core-next/issues/61" +) def test_handles_field_errors_caught_by_graphql(client): response = client.get(url_string(query="{thrower}")) assert response.status_code == 200 @@ -457,6 +467,9 @@ def test_handles_field_errors_caught_by_graphql(client): } +@pytest.mark.xfail( + reason="SourceLocation serialization problem: https://github.com/graphql-python/graphql-core-next/issues/61" +) def test_handles_syntax_errors_caught_by_graphql(client): response = client.get(url_string(query="syntaxerror")) assert response.status_code == 400 From a9d39a4f5ed2d6f2f5f70b8e2bc3e4f0e957b9ab Mon Sep 17 00:00:00 2001 From: Tomasz Kontusz Date: Mon, 16 Sep 2019 05:50:23 +0200 Subject: [PATCH 10/13] Drop support for django-filter < 2 It was only needed for Python 2.7 --- docs/filtering.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/filtering.rst b/docs/filtering.rst index d22d349f9..a511c6459 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -2,8 +2,8 @@ Filtering ========= Graphene integrates with -`django-filter `__ to provide filtering of results. See the `usage -documentation `__ +`django-filter `__ to provide filtering of results. +See the `usage documentation `__ for details on the format for ``filter_fields``. This filtering is automatically available when implementing a ``relay.Node``. From 84ae00f803c874a50780ca753ad77c353f7a7164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Clgen=20Sar=C4=B1kavak?= Date: Sun, 15 Mar 2020 19:43:55 +0300 Subject: [PATCH 11/13] Replace deprecated connection_from_list_slice with connection_from_array_slice connection_from_list_slice: > Deprecated alias for connection_from_array_slice. We're now using the JavaScript terminology in Python as well, since list is too narrow a type and there is no other really appropriate type name. https://github.com/graphql-python/graphql-relay-py/blob/v3.0.0/src/graphql_relay/connection/arrayconnection.py#L54 --- graphene_django/fields.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index a4479d5ce..66b601189 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,11 +1,12 @@ from functools import partial from django.db.models.query import QuerySet +from graphql_relay.connection.arrayconnection import connection_from_array_slice +from promise import Promise + from graphene import NonNull -from graphene.relay import ConnectionField +from graphene.relay import ConnectionField, PageInfo from graphene.types import Field, List -from graphql_relay.connection.arrayconnection import connection_from_list_slice -from promise import Promise from .settings import graphene_settings from .utils import maybe_queryset @@ -121,15 +122,15 @@ def resolve_connection(cls, connection, args, iterable): _len = iterable.count() else: _len = len(iterable) - connection = connection_from_list_slice( + connection = connection_from_array_slice( iterable, args, slice_start=0, - list_length=_len, - list_slice_length=_len, - connection_type=partial(connection_adapter, connection), + connection_type=connection, + array_length=_len, + array_slice_length=_len, edge_type=connection.Edge, - pageinfo_type=page_info_adapter, + page_info_type=PageInfo, ) connection.iterable = iterable connection.length = _len From eb46f0da0af45c9cae3fe295cee2baffbc332fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Clgen=20Sar=C4=B1kavak?= Date: Sun, 15 Mar 2020 19:26:23 +0300 Subject: [PATCH 12/13] Fix remaining graphene 3.0 and graphql 3.0 compatiblity * names must be strings * update to expected schemas to new format * Call get() on django promises to get the actual value --- graphene_django/converter.py | 22 +- graphene_django/debug/middleware.py | 2 +- graphene_django/fields.py | 7 +- graphene_django/filter/tests/test_fields.py | 106 +++-- .../management/commands/graphql_schema.py | 2 +- graphene_django/tests/test_command.py | 4 - graphene_django/tests/test_query.py | 9 +- graphene_django/tests/test_types.py | 407 +++++++++++------- graphene_django/tests/test_views.py | 15 +- graphene_django/views.py | 8 +- 10 files changed, 353 insertions(+), 229 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index ffd3d9483..187874ab4 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -3,11 +3,14 @@ from django.db import models from django.utils.encoding import force_str +from django.utils.functional import Promise from django.utils.module_loading import import_string - from graphene import ( ID, + UUID, Boolean, + Date, + DateTime, Dynamic, Enum, Field, @@ -16,18 +19,16 @@ List, NonNull, String, - UUID, - DateTime, - Date, Time, ) from graphene.types.json import JSONString from graphene.utils.str_converters import to_camel_case, to_const -from graphql import assert_valid_name, GraphQLError +from graphql import GraphQLError, assert_valid_name +from graphql.pyutils import register_description -from .settings import graphene_settings from .compat import ArrayField, HStoreField, JSONField, RangeField -from .fields import DjangoListField, DjangoConnectionField +from .fields import DjangoConnectionField, DjangoListField +from .settings import graphene_settings def convert_choice_name(name): @@ -66,7 +67,7 @@ def convert_choices_to_named_enum_with_descriptions(name, choices): class EnumWithDescriptionsType(object): @property def description(self): - return named_choices_descriptions[self.name] + return str(named_choices_descriptions[self.name]) return Enum(name, list(named_choices), type=EnumWithDescriptionsType) @@ -278,3 +279,8 @@ def convert_postgres_range_to_string(field, registry=None): if not isinstance(inner_type, (List, NonNull)): inner_type = type(inner_type) return List(inner_type, description=field.help_text, required=not field.null) + + +# Register Django lazy()-wrapped values as GraphQL description/help_text. +# This is needed for using lazy translations, see https://github.com/graphql-python/graphql-core-next/issues/58. +register_description(Promise) diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index 0fe3fe39b..8621b55bc 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -17,7 +17,7 @@ def get_debug_promise(self): if not self.debug_promise: self.debug_promise = Promise.all(self.promises) self.promises = [] - return self.debug_promise.then(self.on_resolve_all_promises) + return self.debug_promise.then(self.on_resolve_all_promises).get() def on_resolve_all_promises(self, values): if self.promises: diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 66b601189..418a14b52 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -5,7 +5,8 @@ from promise import Promise from graphene import NonNull -from graphene.relay import ConnectionField, PageInfo +from graphene.relay import ConnectionField +from graphene.relay.connection import connection_adapter, page_info_adapter from graphene.types import Field, List from .settings import graphene_settings @@ -126,11 +127,11 @@ def resolve_connection(cls, connection, args, iterable): iterable, args, slice_start=0, - connection_type=connection, array_length=_len, array_slice_length=_len, + connection_type=partial(connection_adapter, connection), edge_type=connection.Edge, - page_info_type=PageInfo, + page_info_type=page_info_adapter, ) connection.iterable = iterable connection.length = _len diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 166d806fd..59cc30b39 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -806,38 +806,56 @@ class Query(ObjectType): assert str(schema) == dedent( """\ - schema { - query: Query + type Query { + pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null): PetTypeConnection } - interface Node { - id: ID! + type PetTypeConnection { + \"""Pagination data for this connection.\""" + pageInfo: PageInfo! + + \"""Contains the nodes in this connection.\""" + edges: [PetTypeEdge]! } + \""" + The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. + \""" type PageInfo { + \"""When paginating forwards, are there more items?\""" hasNextPage: Boolean! + + \"""When paginating backwards, are there more items?\""" hasPreviousPage: Boolean! + + \"""When paginating backwards, the cursor to continue.\""" startCursor: String + + \"""When paginating forwards, the cursor to continue.\""" endCursor: String } - - type PetType implements Node { - age: Int! - id: ID! - } - - type PetTypeConnection { - pageInfo: PageInfo! - edges: [PetTypeEdge]! - } - + + \"""A Relay edge containing a `PetType` and its cursor.\""" type PetTypeEdge { + \"""The item at the end of the edge\""" node: PetType + + \"""A cursor for use in pagination\""" cursor: String! } - - type Query { - pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection + + type PetType implements Node { + \"""\""" + age: Int! + + \"""The ID of the object\""" + id: ID! + } + + \"""An object with an ID\""" + interface Node { + \"""The ID of the object\""" + id: ID! } """ ) @@ -858,40 +876,58 @@ class Query(ObjectType): assert str(schema) == dedent( """\ - schema { - query: Query + type Query { + pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null, age_Isnull: Boolean = null, age_Lt: Int = null): PetTypeConnection } - interface Node { - id: ID! + type PetTypeConnection { + \"""Pagination data for this connection.\""" + pageInfo: PageInfo! + + \"""Contains the nodes in this connection.\""" + edges: [PetTypeEdge]! } + \""" + The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. + \""" type PageInfo { + \"""When paginating forwards, are there more items?\""" hasNextPage: Boolean! + + \"""When paginating backwards, are there more items?\""" hasPreviousPage: Boolean! - startCursor: String - endCursor: String - } - type PetType implements Node { - age: Int! - id: ID! - } + \"""When paginating backwards, the cursor to continue.\""" + startCursor: String - type PetTypeConnection { - pageInfo: PageInfo! - edges: [PetTypeEdge]! + \"""When paginating forwards, the cursor to continue.\""" + endCursor: String } + \"""A Relay edge containing a `PetType` and its cursor.\""" type PetTypeEdge { + \"""The item at the end of the edge\""" node: PetType + + \"""A cursor for use in pagination\""" cursor: String! } - type Query { - pets(before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection + type PetType implements Node { + \"""\""" + age: Int! + + \"""The ID of the object\""" + id: ID! } - """ + + \"""An object with an ID\""" + interface Node { + \"""The ID of the object\""" + id: ID! + } + """ ) diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py index dcef73c8f..9cf55cadc 100644 --- a/graphene_django/management/commands/graphql_schema.py +++ b/graphene_django/management/commands/graphql_schema.py @@ -56,7 +56,7 @@ def save_json_file(self, out, schema_dict, indent): def save_graphql_file(self, out, schema): with open(out, "w") as outfile: - outfile.write(print_schema(schema)) + outfile.write(print_schema(schema.graphql_schema)) def get_schema(self, schema, out, indent): schema_dict = {"data": schema.introspect()} diff --git a/graphene_django/tests/test_command.py b/graphene_django/tests/test_command.py index f979b5ca6..70116b8ab 100644 --- a/graphene_django/tests/test_command.py +++ b/graphene_django/tests/test_command.py @@ -51,10 +51,6 @@ class Query(ObjectType): schema_output = handle.write.call_args[0][0] assert schema_output == dedent( """\ - schema { - query: Query - } - type Query { hi: String } diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index a5238bf8e..75053dbf4 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -713,7 +713,7 @@ class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) def resolve_all_reporters(self, info, **args): - return Promise.resolve([Reporter(id=1)]) + return Promise.resolve([Reporter(id=1)]).get() schema = graphene.Schema(query=Query) query = """ @@ -842,7 +842,7 @@ class Meta: articles = DjangoConnectionField(ArticleType) def resolve_articles(self, info, **args): - return article_loader.load(self.id) + return article_loader.load(self.id).get() class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) @@ -1075,7 +1075,7 @@ class Meta: class Query(graphene.ObjectType): films = DjangoConnectionField(FilmType) - def resolve_films(root, info, **args): + def resolve_films(root, info, **kwargs): qs = Film.objects.prefetch_related("reporters") return qs @@ -1105,6 +1105,7 @@ def resolve_films(root, info, **args): } """ schema = graphene.Schema(query=Query) + with django_assert_num_queries(3) as captured: result = schema.execute(query) assert not result.errors @@ -1127,7 +1128,7 @@ class Meta: class Query(graphene.ObjectType): films = DjangoConnectionField(FilmType) - def resolve_films(root, info): + def resolve_films(root, info, **kwargs): qs = Film.objects.prefetch_related("reporters") return qs.annotate(reporters_count=models.Count("reporters")) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 9b3ceb616..2a6d357f4 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -111,83 +111,165 @@ class Meta: def test_schema_representation(): - expected = """ -schema { - query: RootQuery -} - -type Article implements Node { - id: ID! - headline: String! - pubDate: Date! - pubDateTime: DateTime! - reporter: Reporter! - editor: Reporter! - lang: ArticleLang! - importance: ArticleImportance -} - -type ArticleConnection { - pageInfo: PageInfo! - edges: [ArticleEdge]! - test: String -} - -type ArticleEdge { - node: Article - cursor: String! -} - -enum ArticleImportance { - A_1 - A_2 -} - -enum ArticleLang { - ES - EN -} - -scalar Date - -scalar DateTime - -interface Node { - id: ID! -} - -type PageInfo { - hasNextPage: Boolean! - hasPreviousPage: Boolean! - startCursor: String - endCursor: String -} - -type Reporter { - id: ID! - firstName: String! - lastName: String! - email: String! - pets: [Reporter!]! - aChoice: ReporterAChoice - reporterType: ReporterReporterType - articles(before: String, after: String, first: Int, last: Int): ArticleConnection! -} - -enum ReporterAChoice { - A_1 - A_2 -} - -enum ReporterReporterType { - A_1 - A_2 -} - -type RootQuery { - node(id: ID!): Node -} -""".lstrip() + expected = dedent( + """\ + schema { + query: RootQuery + } + + \"""Article description\""" + type Article implements Node { + \"""The ID of the object\""" + id: ID! + + \"""\""" + headline: String! + + \"""\""" + pubDate: Date! + + \"""\""" + pubDateTime: DateTime! + + \"""\""" + reporter: Reporter! + + \"""\""" + editor: Reporter! + + \"""Language\""" + lang: ArticleLang! + + \"""\""" + importance: ArticleImportance + } + + \"""An object with an ID\""" + interface Node { + \"""The ID of the object\""" + id: ID! + } + + \""" + The `Date` scalar type represents a Date + value as specified by + [iso8601](https://en.wikipedia.org/wiki/ISO_8601). + \""" + scalar Date + + \""" + The `DateTime` scalar type represents a DateTime + value as specified by + [iso8601](https://en.wikipedia.org/wiki/ISO_8601). + \""" + scalar DateTime + + \"""An enumeration.\""" + enum ArticleLang { + \"""Spanish\""" + ES + + \"""English\""" + EN + } + + \"""An enumeration.\""" + enum ArticleImportance { + \"""Very important\""" + A_1 + + \"""Not as important\""" + A_2 + } + + \"""Reporter description\""" + type Reporter { + \"""\""" + id: ID! + + \"""\""" + firstName: String! + + \"""\""" + lastName: String! + + \"""\""" + email: String! + + \"""\""" + pets: [Reporter!]! + + \"""\""" + aChoice: ReporterAChoice + + \"""\""" + reporterType: ReporterReporterType + + \"""\""" + articles(before: String = null, after: String = null, first: Int = null, last: Int = null): ArticleConnection! + } + + \"""An enumeration.\""" + enum ReporterAChoice { + \"""this\""" + A_1 + + \"""that\""" + A_2 + } + + \"""An enumeration.\""" + enum ReporterReporterType { + \"""Regular\""" + A_1 + + \"""CNN Reporter\""" + A_2 + } + + type ArticleConnection { + \"""Pagination data for this connection.\""" + pageInfo: PageInfo! + + \"""Contains the nodes in this connection.\""" + edges: [ArticleEdge]! + test: String + } + + \""" + The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. + \""" + type PageInfo { + \"""When paginating forwards, are there more items?\""" + hasNextPage: Boolean! + + \"""When paginating backwards, are there more items?\""" + hasPreviousPage: Boolean! + + \"""When paginating backwards, the cursor to continue.\""" + startCursor: String + + \"""When paginating forwards, the cursor to continue.\""" + endCursor: String + } + + \"""A Relay edge containing a `Article` and its cursor.\""" + type ArticleEdge { + \"""The item at the end of the edge\""" + node: Article + + \"""A cursor for use in pagination\""" + cursor: String! + } + + type RootQuery { + node( + \"""The ID of the object\""" + id: ID! + ): Node + } + """ + ) assert str(schema) == expected @@ -415,20 +497,21 @@ class Query(ObjectType): assert str(schema) == dedent( """\ - schema { - query: Query - } + type Query { + pet: Pet + } - type Pet { - id: ID! - kind: String! - cuteness: Int! - } + type Pet { + \"""\""" + id: ID! - type Query { - pet: Pet - } - """ + \"""\""" + kind: String! + + \"""\""" + cuteness: Int! + } + """ ) def test_django_objecttype_convert_choices_enum_list(self, PetModel): @@ -444,25 +527,30 @@ class Query(ObjectType): assert str(schema) == dedent( """\ - schema { - query: Query - } - - type Pet { - id: ID! - kind: PetModelKind! - cuteness: Int! - } - - enum PetModelKind { - CAT - DOG - } - - type Query { - pet: Pet - } - """ + type Query { + pet: Pet + } + + type Pet { + \"""\""" + id: ID! + + \"""\""" + kind: PetModelKind! + + \"""\""" + cuteness: Int! + } + + \"""An enumeration.\""" + enum PetModelKind { + \"""Cat\""" + CAT + + \"""Dog\""" + DOG + } + """ ) def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel): @@ -478,20 +566,21 @@ class Query(ObjectType): assert str(schema) == dedent( """\ - schema { - query: Query - } + type Query { + pet: Pet + } - type Pet { - id: ID! - kind: String! - cuteness: Int! - } + type Pet { + \"""\""" + id: ID! - type Query { - pet: Pet - } - """ + \"""\""" + kind: String! + + \"""\""" + cuteness: Int! + } + """ ) def test_django_objecttype_convert_choices_enum_naming_collisions( @@ -511,24 +600,27 @@ class Query(ObjectType): assert str(schema) == dedent( """\ - schema { - query: Query - } - - type PetModelKind { - id: ID! - kind: TestsPetModelKindChoices! - } - - type Query { - pet: PetModelKind - } - - enum TestsPetModelKindChoices { - CAT - DOG - } - """ + type Query { + pet: PetModelKind + } + + type PetModelKind { + \"""\""" + id: ID! + + \"""\""" + kind: TestsPetModelKindChoices! + } + + \"""An enumeration.\""" + enum TestsPetModelKindChoices { + \"""Cat\""" + CAT + + \"""Dog\""" + DOG + } + """ ) def test_django_objecttype_choices_custom_enum_name( @@ -550,22 +642,25 @@ class Query(ObjectType): assert str(schema) == dedent( """\ - schema { - query: Query - } - - enum CustomEnumKind { - CAT - DOG - } - - type PetModelKind { - id: ID! - kind: CustomEnumKind! - } - - type Query { - pet: PetModelKind - } - """ + type Query { + pet: PetModelKind + } + + type PetModelKind { + \"""\""" + id: ID! + + \"""\""" + kind: CustomEnumKind! + } + + \"""An enumeration.\""" + enum CustomEnumKind { + \"""Cat\""" + CAT + + \"""Dog\""" + DOG + } + """ ) diff --git a/graphene_django/tests/test_views.py b/graphene_django/tests/test_views.py index 5be94f1f3..1c027d99a 100644 --- a/graphene_django/tests/test_views.py +++ b/graphene_django/tests/test_views.py @@ -92,9 +92,6 @@ def test_allows_get_with_operation_name(client): } -@pytest.mark.xfail( - reason="SourceLocation serialization problem: https://github.com/graphql-python/graphql-core-next/issues/61" -) def test_reports_validation_errors(client): response = client.get(url_string(query="{ test, unknownOne, unknownTwo }")) @@ -129,8 +126,8 @@ def test_errors_when_missing_operation_name(client): assert response_json(response) == { "errors": [ { - "locations": None, "message": "Must provide operation name if query contains multiple operations.", + "locations": None, "path": None, } ] @@ -449,9 +446,6 @@ def test_supports_pretty_printing_by_request(client): ) -@pytest.mark.xfail( - reason="SourceLocation serialization problem: https://github.com/graphql-python/graphql-core-next/issues/61" -) def test_handles_field_errors_caught_by_graphql(client): response = client.get(url_string(query="{thrower}")) assert response.status_code == 200 @@ -467,9 +461,6 @@ def test_handles_field_errors_caught_by_graphql(client): } -@pytest.mark.xfail( - reason="SourceLocation serialization problem: https://github.com/graphql-python/graphql-core-next/issues/61" -) def test_handles_syntax_errors_caught_by_graphql(client): response = client.get(url_string(query="syntaxerror")) assert response.status_code == 400 @@ -477,8 +468,8 @@ def test_handles_syntax_errors_caught_by_graphql(client): "errors": [ { "locations": [{"column": 1, "line": 1}], - "message": "Syntax Error GraphQL (1:1) " - 'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n', + "message": "Syntax Error: Unexpected Name 'syntaxerror'.", + "path": None, } ] } diff --git a/graphene_django/views.py b/graphene_django/views.py index 112447e7c..1a373c7a6 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -8,11 +8,10 @@ from django.utils.decorators import method_decorator from django.views.decorators.csrf import ensure_csrf_cookie from django.views.generic import View -from graphql import OperationType, execute, get_operation_ast, parse, validate +from graphql import OperationType, get_operation_ast, parse, validate from graphql.error import GraphQLError from graphql.error import format_error as format_graphql_error from graphql.execution import ExecutionResult -from graphql.type.schema import GraphQLSchema from graphene import Schema @@ -259,9 +258,8 @@ def execute_graphql_request( if validation_errors: return ExecutionResult(data=None, errors=validation_errors) - return execute( - schema=self.schema.graphql_schema, - document=document, + return self.schema.execute( + source=query, root_value=self.get_root_value(request), variable_values=variables, operation_name=operation_name, From 6e223c4e11989589b54699cb9303c5437dd8c2bc Mon Sep 17 00:00:00 2001 From: Jean-Louis Fuchs Date: Wed, 29 Apr 2020 15:25:26 +0200 Subject: [PATCH 13/13] Add is_awaitable to mock_info for compat with latest graphene --- graphene_django/rest_framework/tests/test_mutation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index c2a39478b..1b31e3689 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -24,6 +24,7 @@ def mock_info(): operation=None, variable_values=None, context=None, + is_awaitable=None, )