This commit is contained in:
Ülgen Sarıkavak 2020-04-06 14:17:18 +03:00
parent 1e79165a7e
commit c2e4f4169e
11 changed files with 368 additions and 239 deletions

View File

@ -23,7 +23,7 @@ from graphene import (
) )
from graphene.types.json import JSONString from graphene.types.json import JSONString
from graphene.utils.str_converters import to_camel_case, to_const 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 .settings import graphene_settings
from .compat import ArrayField, HStoreField, JSONField, RangeField from .compat import ArrayField, HStoreField, JSONField, RangeField
@ -34,7 +34,7 @@ def convert_choice_name(name):
name = to_const(force_str(name)) name = to_const(force_str(name))
try: try:
assert_valid_name(name) assert_valid_name(name)
except AssertionError: except GraphQLError:
name = "A_%s" % name name = "A_%s" % name
return name return name
@ -64,7 +64,7 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
class EnumWithDescriptionsType(object): class EnumWithDescriptionsType(object):
@property @property
def description(self): def description(self):
return named_choices_descriptions[self.name] return str(named_choices_descriptions[self.name])
return Enum(name, list(named_choices), type=EnumWithDescriptionsType) return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
@ -276,3 +276,12 @@ def convert_postgres_range_to_string(field, registry=None):
if not isinstance(inner_type, (List, NonNull)): if not isinstance(inner_type, (List, NonNull)):
inner_type = type(inner_type) inner_type = type(inner_type)
return List(inner_type, description=field.help_text, required=not field.null) return List(inner_type, description=field.help_text, required=not field.null)
from django.utils.functional import Promise
from graphql.pyutils import register_description
# 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: if not self.debug_promise:
self.debug_promise = Promise.all(self.promises) self.debug_promise = Promise.all(self.promises)
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): def on_resolve_all_promises(self, values):
if self.promises: if self.promises:

View File

@ -1,12 +1,13 @@
from functools import partial from functools import partial
from django.db.models.query import QuerySet 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_array_slice from graphql_relay.connection.arrayconnection import connection_from_array_slice
from promise import Promise from promise import Promise
from graphene import NonNull from graphene import NonNull
from graphene.relay import ConnectionField, PageInfo from graphene.relay import ConnectionField
from graphene.types import Field, List from graphene.types import Field, List
from .settings import graphene_settings from .settings import graphene_settings
@ -127,11 +128,11 @@ class DjangoConnectionField(ConnectionField):
iterable, iterable,
args, args,
slice_start=0, slice_start=0,
connection_type=connection,
array_length=_len, array_length=_len,
array_slice_length=_len, array_slice_length=_len,
connection_type=partial(connection_adapter, connection),
edge_type=connection.Edge, edge_type=connection.Edge,
page_info_type=PageInfo, page_info_type=page_info_adapter,
) )
connection.iterable = iterable connection.iterable = iterable
connection.length = _len connection.length = _len

View File

@ -806,38 +806,56 @@ def test_integer_field_filter_type():
assert str(schema) == dedent( assert str(schema) == dedent(
"""\ """\
schema { type Query {
query: Query pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null): PetTypeConnection
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PetType implements Node {
age: Int!
id: ID!
} }
type PetTypeConnection { type PetTypeConnection {
\"""Pagination data for this connection.\"""
pageInfo: PageInfo! pageInfo: PageInfo!
\"""Contains the nodes in this connection.\"""
edges: [PetTypeEdge]! 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 { type PetTypeEdge {
\"""The item at the end of the edge\"""
node: PetType node: PetType
\"""A cursor for use in pagination\"""
cursor: String! cursor: String!
} }
type Query { type PetType implements Node {
pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection \"""\"""
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( assert str(schema) == dedent(
"""\ """\
schema { type Query {
query: 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 PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PetType implements Node {
age: Int!
id: ID!
} }
type PetTypeConnection { type PetTypeConnection {
\"""Pagination data for this connection.\"""
pageInfo: PageInfo! pageInfo: PageInfo!
\"""Contains the nodes in this connection.\"""
edges: [PetTypeEdge]! 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 { type PetTypeEdge {
\"""The item at the end of the edge\"""
node: PetType node: PetType
\"""A cursor for use in pagination\"""
cursor: String! cursor: String!
} }
type Query { type PetType implements Node {
pets(before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection \"""\"""
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): def save_graphql_file(self, out, schema):
with open(out, "w") as outfile: 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): def get_schema(self, schema, out, indent):
schema_dict = {"data": schema.introspect()} schema_dict = {"data": schema.introspect()}

View File

@ -17,6 +17,7 @@ def mock_info():
None, None,
None, None,
None, None,
path=None,
schema=None, schema=None,
fragments=None, fragments=None,
root_value=None, root_value=None,

View File

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

View File

@ -612,9 +612,9 @@ def test_should_enforce_first_or_last(graphene_settings):
result = schema.execute(query) result = schema.execute(query)
assert len(result.errors) == 1 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 " "You must provide a `first` or `last` value to properly "
"paginate the `allReporters` connection." "paginate the `allReporters` connection.\n"
) )
assert result.data == expected assert result.data == expected
@ -653,9 +653,9 @@ def test_should_error_if_first_is_greater_than_max(graphene_settings):
result = schema.execute(query) result = schema.execute(query)
assert len(result.errors) == 1 assert len(result.errors) == 1
assert str(result.errors[0]) == ( assert str(result.errors[0]).startswith(
"Requesting 101 records on the `allReporters` connection " "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 assert result.data == expected
@ -694,9 +694,9 @@ def test_should_error_if_last_is_greater_than_max(graphene_settings):
result = schema.execute(query) result = schema.execute(query)
assert len(result.errors) == 1 assert len(result.errors) == 1
assert str(result.errors[0]) == ( assert str(result.errors[0]).startswith(
"Requesting 101 records on the `allReporters` connection " "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 assert result.data == expected
@ -713,7 +713,7 @@ def test_should_query_promise_connectionfields():
all_reporters = DjangoConnectionField(ReporterType) all_reporters = DjangoConnectionField(ReporterType)
def resolve_all_reporters(self, info, **args): 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) schema = graphene.Schema(query=Query)
query = """ query = """
@ -842,7 +842,7 @@ def test_should_query_dataloader_fields():
articles = DjangoConnectionField(ArticleType) articles = DjangoConnectionField(ArticleType)
def resolve_articles(self, info, **args): def resolve_articles(self, info, **args):
return article_loader.load(self.id) return article_loader.load(self.id).get()
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType) all_reporters = DjangoConnectionField(ReporterType)
@ -1105,6 +1105,7 @@ def test_should_preserve_prefetch_related(django_assert_num_queries):
} }
""" """
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
with django_assert_num_queries(3) as captured: with django_assert_num_queries(3) as captured:
result = schema.execute(query) result = schema.execute(query)
assert not result.errors assert not result.errors

View File

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

View File

@ -99,12 +99,14 @@ def test_reports_validation_errors(client):
assert response_json(response) == { assert response_json(response) == {
"errors": [ "errors": [
{ {
"message": 'Cannot query field "unknownOne" on type "QueryRoot".', "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.",
"locations": [{"line": 1, "column": 9}], "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}], "locations": [{"line": 1, "column": 21}],
"path": None,
}, },
] ]
} }
@ -124,7 +126,9 @@ def test_errors_when_missing_operation_name(client):
assert response_json(response) == { assert response_json(response) == {
"errors": [ "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": [ "errors": [
{ {
"locations": [{"column": 1, "line": 1}], "locations": [{"column": 1, "line": 1}],
"message": "Syntax Error GraphQL (1:1) " "message": "Syntax Error: Unexpected Name 'syntaxerror'.",
'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n', "path": None,
} }
] ]
} }

View File

@ -6,14 +6,14 @@ from django.http import HttpResponse, HttpResponseNotAllowed
from django.http.response import HttpResponseBadRequest from django.http.response import HttpResponseBadRequest
from django.shortcuts import render from django.shortcuts import render
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import View
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import View
from graphql import get_default_backend from graphene import Schema
from graphql.error import format_error as format_graphql_error from graphql import parse, get_operation_ast, OperationType, execute, validate
from graphql.error import GraphQLError from graphql.error import GraphQLError
from graphql.error import format_error as format_graphql_error
from graphql.execution import ExecutionResult from graphql.execution import ExecutionResult
from graphql.type.schema import GraphQLSchema
from .settings import graphene_settings from .settings import graphene_settings
@ -66,35 +66,28 @@ class GraphQLView(View):
def __init__( def __init__(
self, self,
schema=None, schema=None,
executor=None,
middleware=None, middleware=None,
root_value=None, root_value=None,
graphiql=False, graphiql=False,
pretty=False, pretty=False,
batch=False, batch=False,
backend=None,
): ):
if not schema: if not schema:
schema = graphene_settings.SCHEMA schema = graphene_settings.SCHEMA
if backend is None:
backend = get_default_backend()
if middleware is None: if middleware is None:
middleware = graphene_settings.MIDDLEWARE middleware = graphene_settings.MIDDLEWARE
self.schema = self.schema or schema self.schema = self.schema or schema
if middleware is not None: if middleware is not None:
self.middleware = list(instantiate_middleware(middleware)) self.middleware = list(instantiate_middleware(middleware))
self.executor = executor
self.root_value = root_value self.root_value = root_value
self.pretty = self.pretty or pretty self.pretty = self.pretty or pretty
self.graphiql = self.graphiql or graphiql self.graphiql = self.graphiql or graphiql
self.batch = self.batch or batch self.batch = self.batch or batch
self.backend = backend
assert isinstance( assert isinstance(
self.schema, GraphQLSchema self.schema, Schema
), "A Schema is required to be provided to GraphQLView." ), "A Schema is required to be provided to GraphQLView."
assert not all((graphiql, batch)), "Use either graphiql or batch processing" assert not all((graphiql, batch)), "Use either graphiql or batch processing"
@ -108,9 +101,6 @@ class GraphQLView(View):
def get_context(self, request): def get_context(self, request):
return request return request
def get_backend(self, request):
return self.backend
@method_decorator(ensure_csrf_cookie) @method_decorator(ensure_csrf_cookie)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
@ -172,7 +162,9 @@ class GraphQLView(View):
self.format_error(e) for e in execution_result.errors 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 status_code = 400
else: else:
response["data"] = execution_result.data response["data"] = execution_result.data
@ -245,14 +237,13 @@ class GraphQLView(View):
raise HttpError(HttpResponseBadRequest("Must provide query string.")) raise HttpError(HttpResponseBadRequest("Must provide query string."))
try: try:
backend = self.get_backend(request) document = parse(query)
document = backend.document_from_string(self.schema, query)
except Exception as e: except Exception as e:
return ExecutionResult(errors=[e], invalid=True) return ExecutionResult(errors=[e])
if request.method.lower() == "get": if request.method.lower() == "get":
operation_type = document.get_operation_type(operation_name) operation_ast = get_operation_ast(document, operation_name)
if operation_type and operation_type != "query": if operation_ast and operation_ast.operation != OperationType.QUERY:
if show_graphiql: if show_graphiql:
return None return None
@ -260,28 +251,23 @@ class GraphQLView(View):
HttpResponseNotAllowed( HttpResponseNotAllowed(
["POST"], ["POST"],
"Can only perform a {} operation from a POST request.".format( "Can only perform a {} operation from a POST request.".format(
operation_type operation_ast.operation.value
), ),
) )
) )
try: validation_errors = validate(self.schema.graphql_schema, document)
extra_options = {} if validation_errors:
if self.executor: return ExecutionResult(data=None, errors=validation_errors)
# We only include it optionally since
# executor is not a valid argument in all backends
extra_options["executor"] = self.executor
return document.execute( return self.schema.execute(
root_value=self.get_root_value(request), source=query,
variable_values=variables, root_value=self.get_root_value(request),
operation_name=operation_name, variable_values=variables,
context_value=self.get_context(request), operation_name=operation_name,
middleware=self.get_middleware(request), context_value=self.get_context(request),
**extra_options middleware=self.get_middleware(request),
) )
except Exception as e:
return ExecutionResult(errors=[e], invalid=True)
@classmethod @classmethod
def can_display_graphiql(cls, request, data): def can_display_graphiql(cls, request, data):