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