graphql 3.0 and graphene 3.0 final rebase (#951)

This commit is contained in:
Jean-Louis Fuchs 2020-05-09 13:13:47 +02:00 committed by GitHub
parent 77b9832606
commit 10d22de98e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 406 additions and 300 deletions

View File

@ -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:

View File

@ -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

View File

@ -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
~~~~~~~~

View File

@ -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

View File

@ -2,8 +2,8 @@ Filtering
=========
Graphene integrates with
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results. See the `usage
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results.
See the `usage documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
for details on the format for ``filter_fields``.
This filtering is automatically available when implementing a ``relay.Node``.

View File

@ -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

View File

@ -3,11 +3,14 @@ from functools import singledispatch
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,25 +19,23 @@ from graphene import (
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
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):
name = to_const(force_str(name))
try:
assert_valid_name(name)
except AssertionError:
except GraphQLError:
name = "A_%s" % name
return name
@ -52,7 +53,9 @@ 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
@ -64,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)
@ -276,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)

View File

@ -17,7 +17,7 @@ class DjangoDebugContext(object):
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:

View File

@ -68,7 +68,7 @@ def test_should_query_nested_field():
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 test_should_query_nested_field():
pets { edges { node { lastName } } }
} } }
}
__debug {
_debug {
sql {
rawSql
}
@ -110,12 +110,12 @@ def test_should_query_nested_field():
)
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"]

View File

@ -1,11 +1,12 @@
from functools import partial
from django.db.models.query import QuerySet
from graphql_relay.connection.arrayconnection import connection_from_list_slice
from graphql_relay.connection.arrayconnection import connection_from_array_slice
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
@ -122,15 +123,15 @@ class DjangoConnectionField(ConnectionField):
_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=connection,
array_length=_len,
array_slice_length=_len,
connection_type=partial(connection_adapter, connection),
edge_type=connection.Edge,
pageinfo_type=PageInfo,
page_info_type=page_info_adapter,
)
connection.iterable = iterable
connection.length = _len

View File

@ -806,38 +806,56 @@ def test_integer_field_filter_type():
assert str(schema) == dedent(
"""\
schema {
query: Query
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PetType implements Node {
age: Int!
id: ID!
type Query {
pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null): PetTypeConnection
}
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
}
\"""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 @@ def test_other_filter_types():
assert str(schema) == dedent(
"""\
schema {
query: Query
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PetType implements Node {
age: Int!
id: ID!
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
}
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
}
\"""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!
}
"""
)

View File

@ -56,7 +56,7 @@ class Command(CommandArguments):
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()}

View File

@ -17,12 +17,14 @@ def mock_info():
None,
None,
None,
path=None,
schema=None,
fragments=None,
root_value=None,
operation=None,
variable_values=None,
context=None,
is_awaitable=None,
)

View File

@ -51,10 +51,6 @@ def test_generate_graphql_file_on_call_graphql_schema():
schema_output = handle.write.call_args[0][0]
assert schema_output == dedent(
"""\
schema {
query: Query
}
type Query {
hi: String
}

View File

@ -612,9 +612,9 @@ def test_should_enforce_first_or_last(graphene_settings):
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 @@ def test_should_error_if_first_is_greater_than_max(graphene_settings):
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 @@ def test_should_error_if_last_is_greater_than_max(graphene_settings):
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
@ -713,7 +713,7 @@ def test_should_query_promise_connectionfields():
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 @@ def test_should_query_dataloader_fields():
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 @@ def test_should_preserve_prefetch_related(django_assert_num_queries):
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
@ -1105,9 +1105,10 @@ def test_should_preserve_prefetch_related(django_assert_num_queries):
}
"""
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():
@ -1127,7 +1128,7 @@ def test_should_preserve_annotations():
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"))
@ -1160,3 +1161,4 @@ def test_should_preserve_annotations():
}
}
assert result.data == expected, str(result.data)
assert not result.errors

View File

@ -111,83 +111,165 @@ def test_django_objecttype_with_custom_meta():
def test_schema_representation():
expected = """
schema {
query: RootQuery
}
expected = dedent(
"""\
schema {
query: RootQuery
}
type Article implements Node {
id: ID!
headline: String!
pubDate: Date!
pubDateTime: DateTime!
reporter: Reporter!
editor: Reporter!
lang: ArticleLang!
importance: ArticleImportance
}
\"""Article description\"""
type Article implements Node {
\"""The ID of the object\"""
id: ID!
type ArticleConnection {
pageInfo: PageInfo!
edges: [ArticleEdge]!
test: String
}
\"""\"""
headline: String!
type ArticleEdge {
node: Article
cursor: String!
}
\"""\"""
pubDate: Date!
enum ArticleImportance {
A_1
A_2
}
\"""\"""
pubDateTime: DateTime!
enum ArticleLang {
ES
EN
}
\"""\"""
reporter: Reporter!
scalar Date
\"""\"""
editor: Reporter!
scalar DateTime
\"""Language\"""
lang: ArticleLang!
interface Node {
id: ID!
}
\"""\"""
importance: ArticleImportance
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
\"""An object with an ID\"""
interface Node {
\"""The ID of the object\"""
id: ID!
}
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!
}
\"""
The `Date` scalar type represents a Date
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
\"""
scalar Date
enum ReporterAChoice {
A_1
A_2
}
\"""
The `DateTime` scalar type represents a DateTime
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
\"""
scalar DateTime
enum ReporterReporterType {
A_1
A_2
}
\"""An enumeration.\"""
enum ArticleLang {
\"""Spanish\"""
ES
type RootQuery {
node(id: ID!): Node
}
""".lstrip()
\"""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 TestDjangoObjectType:
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 TestDjangoObjectType:
assert str(schema) == dedent(
"""\
schema {
query: Query
}
type Query {
pet: Pet
}
type Pet {
id: ID!
kind: PetModelKind!
cuteness: Int!
}
type Pet {
\"""\"""
id: ID!
enum PetModelKind {
CAT
DOG
}
\"""\"""
kind: PetModelKind!
type Query {
pet: Pet
}
"""
\"""\"""
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 TestDjangoObjectType:
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 TestDjangoObjectType:
assert str(schema) == dedent(
"""\
schema {
query: Query
}
type Query {
pet: PetModelKind
}
type PetModelKind {
id: ID!
kind: TestsPetModelKindChoices!
}
type PetModelKind {
\"""\"""
id: ID!
type Query {
pet: PetModelKind
}
\"""\"""
kind: TestsPetModelKindChoices!
}
enum TestsPetModelKindChoices {
CAT
DOG
}
"""
\"""An enumeration.\"""
enum TestsPetModelKindChoices {
\"""Cat\"""
CAT
\"""Dog\"""
DOG
}
"""
)
def test_django_objecttype_choices_custom_enum_name(
@ -550,22 +642,25 @@ class TestDjangoObjectType:
assert str(schema) == dedent(
"""\
schema {
query: Query
}
type Query {
pet: PetModelKind
}
enum CustomEnumKind {
CAT
DOG
}
type PetModelKind {
\"""\"""
id: ID!
type PetModelKind {
id: ID!
kind: CustomEnumKind!
}
\"""\"""
kind: CustomEnumKind!
}
type Query {
pet: PetModelKind
}
"""
\"""An enumeration.\"""
enum CustomEnumKind {
\"""Cat\"""
CAT
\"""Dog\"""
DOG
}
"""
)

View File

@ -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."
"message": "Must provide operation name if query contains multiple operations.",
"locations": None,
"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,
}
]
}

View File

@ -6,14 +6,14 @@ from django.http import HttpResponse, HttpResponseNotAllowed
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, 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 +56,6 @@ class GraphQLView(View):
schema = None
graphiql = False
executor = None
backend = None
middleware = None
root_value = None
pretty = False
@ -66,35 +64,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 +99,6 @@ class GraphQLView(View):
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 +160,9 @@ class GraphQLView(View):
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 +235,13 @@ class GraphQLView(View):
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 +249,23 @@ class GraphQLView(View):
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
validation_errors = validate(self.schema.graphql_schema, document)
if validation_errors:
return ExecutionResult(data=None, errors=validation_errors)
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)
return self.schema.execute(
source=query,
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):

View File

@ -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 @@ setup(
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"],

View File

@ -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