diff --git a/.travis.yml b/.travis.yml index 871d4e3bd..4d1c8d6fe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,18 +15,6 @@ after_success: matrix: fast_finish: true include: - - python: 2.7 - env: DJANGO=1.11 - - - python: 3.5 - env: DJANGO=1.11 - - python: 3.5 - env: DJANGO=2.0 - - python: 3.5 - env: DJANGO=2.1 - - python: 3.5 - env: DJANGO=2.2 - - python: 3.6 env: DJANGO=1.11 - python: 3.6 diff --git a/docs/authorization.rst b/docs/authorization.rst index ebc979581..1b7ab55d7 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -166,7 +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.9 and below: +For Django 1.11: .. code:: python @@ -184,4 +184,4 @@ For Django 2.0 and above: path('graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)), ] -.. _LoginRequiredMixin: https://docs.djangoproject.com/en/1.10/topics/auth/default/#the-loginrequired-mixin +.. _LoginRequiredMixin: https://docs.djangoproject.com/en/1.11/topics/auth/default/#the-loginrequired-mixin diff --git a/docs/filtering.rst b/docs/filtering.rst index 6fe7cabaa..67379333a 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -2,9 +2,8 @@ Filtering ========= Graphene integrates with -`django-filter `__ (2.x for -Python 3 or 1.x for Python 2) 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``. diff --git a/graphene_django/converter.py b/graphene_django/converter.py index b59c906fb..6841e690f 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -1,6 +1,8 @@ from collections import OrderedDict from django.db import models from django.utils.encoding import force_text +from django.utils.functional import Promise +from functools import singledispatch from graphene import ( ID, @@ -20,20 +22,18 @@ ) 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 graphql.pyutils import register_description from .compat import ArrayField, HStoreField, JSONField, RangeField from .fields import DjangoListField, DjangoConnectionField -from .utils import import_single_dispatch - -singledispatch = import_single_dispatch() def convert_choice_name(name): name = to_const(force_text(name)) try: assert_valid_name(name) - except AssertionError: + except GraphQLError: name = "A_%s" % name return name @@ -252,3 +252,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/tests/test_query.py b/graphene_django/debug/tests/test_query.py index db8f2754b..cbea0b853 100644 --- a/graphene_django/debug/tests/test_query.py +++ b/graphene_django/debug/tests/test_query.py @@ -15,10 +15,10 @@ class context(object): # from examples.starwars_django.models import Character -pytestmark = pytest.mark.django_db +pytestmark = [pytest.mark.django_db, pytest.mark.asyncio] -def test_should_query_field(): +async def test_should_query_field(): r1 = Reporter(last_name="ABA") r1.save() r2 = Reporter(last_name="Griffin") @@ -53,14 +53,14 @@ def resolve_reporter(self, info, **args): "_debug": {"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]}, } schema = graphene.Schema(query=Query) - result = schema.execute( + result = await schema.execute_async( query, context_value=context(), middleware=[DjangoDebugMiddleware()] ) assert not result.errors assert result.data == expected -def test_should_query_nested_field(): +async def test_should_query_nested_field(): r1 = Reporter(last_name="ABA") r1.save() r2 = Reporter(last_name="Griffin") @@ -75,7 +75,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() @@ -89,7 +89,7 @@ def resolve_reporter(self, info, **args): pets { edges { node { lastName } } } } } } } - __debug { + _debug { sql { rawSql } @@ -112,22 +112,22 @@ def resolve_reporter(self, info, **args): } } schema = graphene.Schema(query=Query) - result = schema.execute( + result = await schema.execute_async( query, context_value=context(), middleware=[DjangoDebugMiddleware()] ) 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"] -def test_should_query_list(): +async def test_should_query_list(): r1 = Reporter(last_name="ABA") r1.save() r2 = Reporter(last_name="Griffin") @@ -162,14 +162,14 @@ def resolve_all_reporters(self, info, **args): "_debug": {"sql": [{"rawSql": str(Reporter.objects.all().query)}]}, } schema = graphene.Schema(query=Query) - result = schema.execute( + result = await schema.execute_async( query, context_value=context(), middleware=[DjangoDebugMiddleware()] ) assert not result.errors assert result.data == expected -def test_should_query_connection(): +async def test_should_query_connection(): r1 = Reporter(last_name="ABA") r1.save() r2 = Reporter(last_name="Griffin") @@ -205,7 +205,7 @@ def resolve_all_reporters(self, info, **args): """ expected = {"allReporters": {"edges": [{"node": {"lastName": "ABA"}}]}} schema = graphene.Schema(query=Query) - result = schema.execute( + result = await schema.execute_async( query, context_value=context(), middleware=[DjangoDebugMiddleware()] ) assert not result.errors @@ -215,7 +215,7 @@ def resolve_all_reporters(self, info, **args): assert result.data["_debug"]["sql"][1]["rawSql"] == query -def test_should_query_connectionfilter(): +async def test_should_query_connectionfilter(): from ...filter import DjangoFilterConnectionField r1 = Reporter(last_name="ABA") @@ -254,7 +254,7 @@ def resolve_all_reporters(self, info, **args): """ expected = {"allReporters": {"edges": [{"node": {"lastName": "ABA"}}]}} schema = graphene.Schema(query=Query) - result = schema.execute( + result = await schema.execute_async( query, context_value=context(), middleware=[DjangoDebugMiddleware()] ) assert not result.errors diff --git a/graphene_django/fields.py b/graphene_django/fields.py index e6daa889e..d3eaaffdd 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,11 +1,13 @@ from functools import partial from django.db.models.query import QuerySet +from graphene.relay.connection import page_info_adapter, connection_adapter + 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 .settings import graphene_settings @@ -136,9 +138,9 @@ def resolve_connection(cls, connection, default_manager, 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 diff --git a/graphene_django/filter/filterset.py b/graphene_django/filter/filterset.py index 7676ea85b..34108aea4 100644 --- a/graphene_django/filter/filterset.py +++ b/graphene_django/filter/filterset.py @@ -1,7 +1,7 @@ import itertools from django.db import models -from django_filters import Filter, MultipleChoiceFilter, VERSION +from django_filters import Filter, MultipleChoiceFilter from django_filters.filterset import BaseFilterSet, FilterSet from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS @@ -50,36 +50,6 @@ class GrapheneFilterSetMixin(BaseFilterSet): ) -# To support a Django 1.11 + Python 2.7 combination django-filter must be -# < 2.x.x. To support the earlier version of django-filter, the -# filter_for_reverse_field method must be present on GrapheneFilterSetMixin and -# must not be present for later versions of django-filter. -if VERSION[0] < 2: - from django.utils.text import capfirst - - class GrapheneFilterSetMixinPython2(GrapheneFilterSetMixin): - @classmethod - def filter_for_reverse_field(cls, f, name): - """Handles retrieving filters for reverse relationships - We override the default implementation so that we can handle - Global IDs (the default implementation expects database - primary keys) - """ - try: - rel = f.field.remote_field - except AttributeError: - rel = f.field.rel - default = {"name": name, "label": capfirst(rel.related_name)} - if rel.multiple: - # For to-many relationships - return GlobalIDMultipleChoiceFilter(**default) - else: - # For to-one relationships - return GlobalIDFilter(**default) - - GrapheneFilterSetMixin = GrapheneFilterSetMixinPython2 - - def setup_filterset(filterset_class): """ Wrap a provided filterset in Graphene-specific functionality """ diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 1ffa0f452..4a683a314 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -655,8 +655,8 @@ def resolve_all_reporters(self, info, **args): result = schema.execute(query) assert len(result.errors) == 1 - assert str(result.errors[0]) == ( - "Received two sliced querysets (high mark) in the connection, please slice only in one." + assert str(result.errors[0]).startswith( + "Received two sliced querysets (high mark) in the connection, please slice only in one.\n" ) @@ -781,38 +781,55 @@ class Query(ObjectType): assert str(schema) == dedent( """\ - schema { - query: Query - } - + \"""An object with an ID\""" interface Node { + \"""The ID of the object\""" id: ID! } + \""" + 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! + + \"""The ID of the object\""" id: ID! } type PetTypeConnection { + \"""Pagination data for this connection.\""" pageInfo: PageInfo! + + \"""Contains the nodes in this connection.\""" 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 + pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null): PetTypeConnection } """ ) @@ -833,38 +850,55 @@ class Query(ObjectType): assert str(schema) == dedent( """\ - schema { - query: Query - } - + \"""An object with an ID\""" interface Node { + \"""The ID of the object\""" id: ID! } + \""" + 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! + + \"""The ID of the object\""" id: ID! } type PetTypeConnection { + \"""Pagination data for this connection.\""" pageInfo: PageInfo! + + \"""Contains the nodes in this connection.\""" 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, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection + 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 } """ ) diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 891645618..c91268515 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -1,13 +1,10 @@ from django import forms from django.core.exceptions import ImproperlyConfigured +from functools import singledispatch from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField -from ..utils import import_single_dispatch - - -singledispatch = import_single_dispatch() @singledispatch diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index caeb7dd52..91d82a58f 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -1,15 +1,13 @@ from django.core.exceptions import ImproperlyConfigured from rest_framework import serializers +from functools import singledispatch import graphene from ..registry import get_global_registry from ..converter import convert_choices_to_named_enum_with_descriptions -from ..utils import import_single_dispatch from .types import DictType -singledispatch = import_single_dispatch() - @singledispatch def get_graphene_type_from_serializer_field(field): diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index 9d8b95022..8adf45726 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -18,6 +18,7 @@ def mock_info(): None, None, None, + path=None, schema=None, fragments=None, root_value=None, diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index f24f84bca..d072fc9b0 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -620,9 +620,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 @@ -659,9 +659,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 @@ -700,16 +700,17 @@ 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 graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False -def test_should_query_promise_connectionfields(): +@pytest.mark.asyncio +async def test_should_query_promise_connectionfields(): from promise import Promise class ReporterType(DjangoObjectType): @@ -738,7 +739,7 @@ def resolve_all_reporters(self, info, **args): expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} - result = schema.execute(query) + result = await schema.execute_async(query) assert not result.errors assert result.data == expected @@ -821,7 +822,8 @@ def resolve_all_reporters(self, info, **args): assert result.data == expected -def test_should_query_dataloader_fields(): +@pytest.mark.asyncio +async def test_should_query_dataloader_fields(): from promise import Promise from promise.dataloader import DataLoader @@ -914,7 +916,7 @@ class Query(graphene.ObjectType): } } - result = schema.execute(query) + result = await schema.execute_async(query) assert not result.errors assert result.data == expected @@ -1083,7 +1085,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 @@ -1115,4 +1117,4 @@ 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 diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 5e9d1c232..ef4bed828 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -118,53 +118,95 @@ def test_schema_representation(): 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 } type ArticleConnection { + \"""Pagination data for this connection.\""" pageInfo: PageInfo! + + \"""Contains the nodes in this connection.\""" edges: [ArticleEdge]! test: 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! } +\"""An enumeration.\""" enum ArticleImportance { + \"""Very important\""" A_1 + + \"""Not as important\""" A_2 } +\"""An enumeration.\""" enum ArticleLang { + \"""Spanish\""" ES + + \"""English\""" EN } +\""" +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 object with an ID\""" interface Node { + \"""The ID of the object\""" id: ID! } +\""" +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 } +\"""Reporter description\""" type Reporter { id: ID! firstName: String! @@ -173,20 +215,29 @@ def test_schema_representation(): pets: [Reporter!]! aChoice: ReporterAChoice reporterType: ReporterReporterType - articles(before: String, after: String, first: Int, last: Int): ArticleConnection! + 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 RootQuery { + \"""The ID of the object\""" node(id: ID!): Node } """.lstrip() @@ -346,10 +397,6 @@ class Query(ObjectType): assert str(schema) == dedent( """\ - schema { - query: Query - } - type Pet { id: ID! kind: String! @@ -375,18 +422,18 @@ class Query(ObjectType): assert str(schema) == dedent( """\ - schema { - query: Query - } - type Pet { id: ID! kind: PetModelKind! cuteness: Int! } + \"""An enumeration.\""" enum PetModelKind { + \"""Cat\""" CAT + + \"""Dog\""" DOG } @@ -409,10 +456,6 @@ class Query(ObjectType): assert str(schema) == dedent( """\ - schema { - query: Query - } - type Pet { id: ID! kind: String! diff --git a/graphene_django/tests/test_views.py b/graphene_django/tests/test_views.py index db6cc4e80..46df966ef 100644 --- a/graphene_django/tests/test_views.py +++ b/graphene_django/tests/test_views.py @@ -99,12 +99,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 +126,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, } ] } @@ -464,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/utils/__init__.py b/graphene_django/utils/__init__.py index 9d8658b1f..671b0609a 100644 --- a/graphene_django/utils/__init__.py +++ b/graphene_django/utils/__init__.py @@ -4,7 +4,6 @@ camelize, get_model_fields, get_reverse_fields, - import_single_dispatch, is_valid_django_model, maybe_queryset, ) @@ -16,6 +15,5 @@ "get_model_fields", "camelize", "is_valid_django_model", - "import_single_dispatch", "GraphQLTestCase", ] diff --git a/graphene_django/utils/utils.py b/graphene_django/utils/utils.py index 47c0c37d0..49b4c4f71 100644 --- a/graphene_django/utils/utils.py +++ b/graphene_django/utils/utils.py @@ -77,26 +77,3 @@ def get_model_fields(model): def is_valid_django_model(model): return inspect.isclass(model) and issubclass(model, models.Model) - - -def import_single_dispatch(): - try: - from functools import singledispatch - except ImportError: - singledispatch = None - - if not singledispatch: - try: - from singledispatch import singledispatch - except ImportError: - pass - - if not singledispatch: - raise Exception( - "It seems your python version does not include " - "functools.singledispatch. Please install the 'singledispatch' " - "package. More information here: " - "https://pypi.python.org/pypi/singledispatch" - ) - - return singledispatch diff --git a/graphene_django/views.py b/graphene_django/views.py index d2c832412..e2aa688e7 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -9,8 +9,9 @@ from django.utils.decorators import method_decorator from django.views.generic import View from django.views.decorators.csrf import ensure_csrf_cookie +from graphene import Schema +from graphql import parse, get_operation_ast, OperationType, execute, validate -from graphql import get_default_backend from graphql.error import format_error as format_graphql_error from graphql.error import GraphQLError from graphql.execution import ExecutionResult @@ -55,10 +56,8 @@ class GraphQLView(View): graphiql_template = "graphene/graphiql.html" react_version = "16.8.6" - schema = None + schema: Schema = None graphiql = False - executor = None - backend = None middleware = None root_value = None pretty = False @@ -67,35 +66,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" @@ -109,9 +101,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: @@ -173,7 +162,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 @@ -246,14 +237,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 @@ -261,28 +251,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=self.get_root_value(request), - variables=variables, - operation_name=operation_name, - context=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): diff --git a/setup.py b/setup.py index a3d0b749c..3b5f91386 100644 --- a/setup.py +++ b/setup.py @@ -16,11 +16,11 @@ tests_require = [ "pytest>=3.6.3", "pytest-cov", + "pytest-asyncio", "coveralls", "mock", "pytz", - "django-filter<2;python_version<'3'", - "django-filter>=2;python_version>='3'", + "django-filter>=2", "pytest-django>=3.3.2", ] + rest_framework_require @@ -45,22 +45,18 @@ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: PyPy", ], keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests"]), install_requires=[ "six>=1.10.0", - "graphene>=2.1.7,<3", - "graphql-core>=2.1.0,<3", + "graphene>=3.0.dev,<4", + "graphql-core>=3.0.0b1,<4", "Django>=1.11", - "singledispatch>=3.4.0.3", "promise>=2.1", ], setup_requires=["pytest-runner"], diff --git a/tox.ini b/tox.ini index a1b599a38..b3fe18780 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,35,36,37}-django{111,20,21,22,master}, + py{36,37}-django{111,20,21,22,master}, black,flake8 [travis:env] @@ -18,7 +18,7 @@ setenv = DJANGO_SETTINGS_MODULE=django_test_settings deps = -e.[test] - psycopg2 + psycopg2-binary django111: Django>=1.11,<2.0 django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2